From 09b4c0177177300665abc08965df3035132826a3 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 13:37:17 +0200 Subject: [PATCH 01/62] Set up initial project structure, add WebDraft editor core, layer manager, color picker, and Vite configuration. --- .idea/.gitignore | 10 + .idea/WebDraft.iml | 8 + .idea/modules.xml | 8 + .idea/php.xml | 19 + .idea/vcs.xml | 6 + app-v3/.gitignore | 2 + app-v3/index.html | 12 + app-v3/package-lock.json | 1002 ++++++++++++++++++++++++++++ app-v3/package.json | 15 + app-v3/src/core/layer-manager.ts | 57 ++ app-v3/src/core/types.ts | 19 + app-v3/src/core/webdraft-editor.ts | 135 ++++ app-v3/src/main.ts | 35 + app-v3/src/styles.css | 183 +++++ app-v3/src/ui/color-picker.ts | 29 + app-v3/src/ui/toolbar.ts | 88 +++ app-v3/tsconfig.json | 16 + docs/rewrite-checklist.md | 35 + 18 files changed, 1679 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/WebDraft.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/php.xml create mode 100644 .idea/vcs.xml create mode 100644 app-v3/.gitignore create mode 100644 app-v3/index.html create mode 100644 app-v3/package-lock.json create mode 100644 app-v3/package.json create mode 100644 app-v3/src/core/layer-manager.ts create mode 100644 app-v3/src/core/types.ts create mode 100644 app-v3/src/core/webdraft-editor.ts create mode 100644 app-v3/src/main.ts create mode 100644 app-v3/src/styles.css create mode 100644 app-v3/src/ui/color-picker.ts create mode 100644 app-v3/src/ui/toolbar.ts create mode 100644 app-v3/tsconfig.json create mode 100644 docs/rewrite-checklist.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/WebDraft.iml b/.idea/WebDraft.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/WebDraft.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f4cf887 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app-v3/.gitignore b/app-v3/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/app-v3/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/app-v3/index.html b/app-v3/index.html new file mode 100644 index 0000000..3321ef7 --- /dev/null +++ b/app-v3/index.html @@ -0,0 +1,12 @@ + + + + + + WebDraft v3 + + +
+ + + diff --git a/app-v3/package-lock.json b/app-v3/package-lock.json new file mode 100644 index 0000000..0d31313 --- /dev/null +++ b/app-v3/package-lock.json @@ -0,0 +1,1002 @@ +{ + "name": "webdraft-v3", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webdraft-v3", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.5.4", + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/app-v3/package.json b/app-v3/package.json new file mode 100644 index 0000000..48c5b66 --- /dev/null +++ b/app-v3/package.json @@ -0,0 +1,15 @@ +{ + "name": "webdraft-v3", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc && vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "devDependencies": { + "typescript": "^5.5.4", + "vite": "^5.4.0" + } +} diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts new file mode 100644 index 0000000..079d5da --- /dev/null +++ b/app-v3/src/core/layer-manager.ts @@ -0,0 +1,57 @@ +export type Layer = { + id: string; + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + visible: boolean; +}; + +export class LayerManager { + private readonly root: HTMLElement; + private readonly layers: Layer[] = []; + private activeLayerId = ''; + + constructor(root: HTMLElement) { + this.root = root; + } + + get activeLayer(): Layer { + const layer = this.layers.find((item) => item.id === this.activeLayerId); + + if (!layer) { + throw new Error('No active drawing layer.'); + } + + return layer; + } + + createLayer(width: number, height: number): Layer { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Canvas 2D context is unavailable.'); + } + + canvas.width = width; + canvas.height = height; + canvas.className = 'drawing-layer'; + + const layer: Layer = { + id: crypto.randomUUID(), + canvas, + context, + visible: true + }; + + this.layers.push(layer); + this.activeLayerId = layer.id; + this.root.append(canvas); + + return layer; + } + + clearActiveLayer(): void { + const layer = this.activeLayer; + layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + } +} diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts new file mode 100644 index 0000000..e6c1968 --- /dev/null +++ b/app-v3/src/core/types.ts @@ -0,0 +1,19 @@ +export type ToolId = 'pencil' | 'eraser'; + +export type Point = { + x: number; + y: number; +}; + +export type EditorOptions = { + width: number; + height: number; + color: string; + size: number; +}; + +export type EditorState = { + activeTool: ToolId; + color: string; + size: number; +}; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts new file mode 100644 index 0000000..778dce0 --- /dev/null +++ b/app-v3/src/core/webdraft-editor.ts @@ -0,0 +1,135 @@ +import { LayerManager } from './layer-manager'; +import type { EditorOptions, EditorState, Point, ToolId } from './types'; + +export class WebDraftEditor extends EventTarget { + private readonly root: HTMLElement; + private readonly options: EditorOptions; + private readonly layerManager: LayerManager; + private readonly eventLayer: HTMLDivElement; + private isDrawing = false; + private lastPoint: Point | null = null; + + readonly state: EditorState; + + constructor(root: HTMLElement, options: EditorOptions) { + super(); + + this.root = root; + this.options = options; + this.layerManager = new LayerManager(root); + this.eventLayer = document.createElement('div'); + this.eventLayer.className = 'event-layer'; + + this.state = { + activeTool: 'pencil', + color: options.color, + size: options.size + }; + } + + mount(): void { + this.root.style.setProperty('--canvas-width', `${this.options.width}px`); + this.root.style.setProperty('--canvas-height', `${this.options.height}px`); + this.layerManager.createLayer(this.options.width, this.options.height); + this.root.append(this.eventLayer); + this.bindPointerEvents(); + } + + setTool(tool: ToolId): void { + this.state.activeTool = tool; + this.dispatchChange(); + } + + setColor(color: string): void { + this.state.color = color; + this.dispatchChange(); + } + + setSize(size: number): void { + this.state.size = size; + this.dispatchChange(); + } + + clear(): void { + this.layerManager.clearActiveLayer(); + } + + private bindPointerEvents(): void { + this.eventLayer.addEventListener('pointerdown', (event) => { + this.eventLayer.setPointerCapture(event.pointerId); + this.isDrawing = true; + this.lastPoint = this.getPoint(event); + this.drawPoint(this.lastPoint); + }); + + this.eventLayer.addEventListener('pointermove', (event) => { + if (!this.isDrawing || !this.lastPoint) { + return; + } + + const nextPoint = this.getPoint(event); + this.drawLine(this.lastPoint, nextPoint); + this.lastPoint = nextPoint; + }); + + this.eventLayer.addEventListener('pointerup', (event) => { + this.eventLayer.releasePointerCapture(event.pointerId); + this.isDrawing = false; + this.lastPoint = null; + }); + + this.eventLayer.addEventListener('pointercancel', () => { + this.isDrawing = false; + this.lastPoint = null; + }); + } + + private getPoint(event: PointerEvent): Point { + const rect = this.eventLayer.getBoundingClientRect(); + + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + } + + private drawPoint(point: Point): void { + const { context } = this.layerManager.activeLayer; + this.applyBrush(context); + + context.beginPath(); + context.arc(point.x, point.y, this.state.size / 2, 0, Math.PI * 2); + context.fill(); + } + + private drawLine(start: Point, end: Point): void { + const { context } = this.layerManager.activeLayer; + this.applyBrush(context); + + context.beginPath(); + context.moveTo(start.x, start.y); + context.lineTo(end.x, end.y); + context.stroke(); + } + + private applyBrush(context: CanvasRenderingContext2D): void { + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.lineWidth = this.state.size; + + if (this.state.activeTool === 'eraser') { + context.globalCompositeOperation = 'destination-out'; + context.strokeStyle = '#000000'; + context.fillStyle = '#000000'; + return; + } + + context.globalCompositeOperation = 'source-over'; + context.strokeStyle = this.state.color; + context.fillStyle = this.state.color; + } + + private dispatchChange(): void { + this.dispatchEvent(new CustomEvent('change', { detail: this.state })); + } +} diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts new file mode 100644 index 0000000..3497421 --- /dev/null +++ b/app-v3/src/main.ts @@ -0,0 +1,35 @@ +import { WebDraftEditor } from './core/webdraft-editor'; +import { createToolbar } from './ui/toolbar'; +import './styles.css'; + +const app = document.querySelector('#app'); + +if (!app) { + throw new Error('Missing #app root element.'); +} + +const shell = document.createElement('main'); +shell.className = 'app-shell'; + +const sidebar = document.createElement('aside'); +sidebar.className = 'sidebar'; + +const workspace = document.createElement('section'); +workspace.className = 'workspace'; + +const surface = document.createElement('div'); +surface.className = 'canvas-surface'; + +workspace.append(surface); +shell.append(sidebar, workspace); +app.append(shell); + +const editor = new WebDraftEditor(surface, { + width: 900, + height: 620, + color: '#111111', + size: 10 +}); + +sidebar.append(createToolbar(editor)); +editor.mount(); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css new file mode 100644 index 0000000..c799070 --- /dev/null +++ b/app-v3/src/styles.css @@ -0,0 +1,183 @@ +:root { + color: #f4f7fb; + background: #202327; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: #202327; +} + +button, +input { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + border-right: 1px solid #343941; + background: #17191d; +} + +.workspace { + display: grid; + place-items: center; + min-width: 0; + overflow: auto; + padding: 32px; +} + +.canvas-surface { + position: relative; + width: var(--canvas-width); + height: var(--canvas-height); + max-width: 100%; + max-height: calc(100vh - 64px); + background-color: #f9fafb; + background-image: + linear-gradient(45deg, #d6dae0 25%, transparent 25%), + linear-gradient(-45deg, #d6dae0 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #d6dae0 75%), + linear-gradient(-45deg, transparent 75%, #d6dae0 75%); + background-position: 0 0, 0 10px, 10px -10px, -10px 0; + background-size: 20px 20px; + border: 1px solid #4b89ff; + box-shadow: 0 24px 80px rgb(0 0 0 / 30%); + overflow: hidden; +} + +.drawing-layer, +.event-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.event-layer { + touch-action: none; + cursor: crosshair; +} + +.toolbar { + display: grid; + gap: 18px; + padding: 18px; +} + +.toolbar h1 { + margin: 0; + font-size: 18px; + font-weight: 700; +} + +.toolbar__group { + display: grid; + grid-template-columns: repeat(4, 44px); + gap: 8px; +} + +.tool-button, +.command-button { + min-height: 40px; + border: 1px solid #343941; + border-radius: 6px; + color: #f4f7fb; + background: #242832; + cursor: pointer; +} + +.tool-button { + width: 44px; + height: 44px; +} + +.tool-button:hover, +.command-button:hover { + border-color: #5b9cff; +} + +.tool-button.is-active { + border-color: #5b9cff; + background: #173d76; +} + +.color-picker, +.range-control { + display: grid; + gap: 8px; + color: #c9d1dc; + font-size: 13px; +} + +.color-picker { + grid-template-columns: 24px 1fr; + align-items: center; + cursor: pointer; +} + +.color-picker input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.color-picker__swatch { + width: 24px; + height: 24px; + border: 1px solid #59606d; + border-radius: 50%; +} + +.range-control { + grid-template-columns: 1fr auto; +} + +.range-control input { + grid-column: 1 / -1; + width: 100%; +} + +.command-button { + width: 100%; + padding: 0 12px; +} + +@media (max-width: 760px) { + .app-shell { + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid #343941; + } + + .toolbar { + grid-template-columns: auto auto 1fr auto; + align-items: center; + } + + .toolbar__group { + grid-template-columns: repeat(2, 44px); + } + + .workspace { + padding: 16px; + } +} diff --git a/app-v3/src/ui/color-picker.ts b/app-v3/src/ui/color-picker.ts new file mode 100644 index 0000000..ed40e48 --- /dev/null +++ b/app-v3/src/ui/color-picker.ts @@ -0,0 +1,29 @@ +export type ColorPickerOptions = { + label: string; + value: string; + onChange: (color: string) => void; +}; + +export function createColorPicker(options: ColorPickerOptions): HTMLElement { + const wrapper = document.createElement('label'); + wrapper.className = 'color-picker'; + + const swatch = document.createElement('span'); + swatch.className = 'color-picker__swatch'; + swatch.style.backgroundColor = options.value; + + const text = document.createElement('span'); + text.textContent = options.label; + + const input = document.createElement('input'); + input.type = 'color'; + input.value = options.value; + input.addEventListener('input', () => { + swatch.style.backgroundColor = input.value; + options.onChange(input.value); + }); + + wrapper.append(swatch, text, input); + + return wrapper; +} diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts new file mode 100644 index 0000000..74f65a8 --- /dev/null +++ b/app-v3/src/ui/toolbar.ts @@ -0,0 +1,88 @@ +import type { WebDraftEditor } from '../core/webdraft-editor'; +import type { ToolId } from '../core/types'; +import { createColorPicker } from './color-picker'; + +type ToolConfig = { + id: ToolId; + label: string; + icon: string; +}; + +const tools: ToolConfig[] = [ + { id: 'pencil', label: 'Pencil', icon: 'P' }, + { id: 'eraser', label: 'Eraser', icon: 'E' } +]; + +export function createToolbar(editor: WebDraftEditor): HTMLElement { + const toolbar = document.createElement('div'); + toolbar.className = 'toolbar'; + + const title = document.createElement('h1'); + title.textContent = 'WebDraft'; + + const toolGroup = document.createElement('div'); + toolGroup.className = 'toolbar__group'; + + const toolButtons = new Map(); + + for (const tool of tools) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'tool-button'; + button.dataset.tool = tool.id; + button.title = tool.label; + button.textContent = tool.icon; + + button.addEventListener('click', () => { + editor.setTool(tool.id); + }); + + toolButtons.set(tool.id, button); + toolGroup.append(button); + } + + const sizeControl = document.createElement('label'); + sizeControl.className = 'range-control'; + sizeControl.textContent = 'Size'; + + const sizeValue = document.createElement('span'); + sizeValue.textContent = String(editor.state.size); + + const sizeInput = document.createElement('input'); + sizeInput.type = 'range'; + sizeInput.min = '1'; + sizeInput.max = '120'; + sizeInput.value = String(editor.state.size); + sizeInput.addEventListener('input', () => { + const size = Number(sizeInput.value); + sizeValue.textContent = String(size); + editor.setSize(size); + }); + + sizeControl.append(sizeInput, sizeValue); + + const colorPicker = createColorPicker({ + label: 'Color', + value: editor.state.color, + onChange: (color) => editor.setColor(color) + }); + + const clearButton = document.createElement('button'); + clearButton.type = 'button'; + clearButton.className = 'command-button'; + clearButton.textContent = 'Clear'; + clearButton.addEventListener('click', () => editor.clear()); + + const renderState = () => { + for (const [tool, button] of toolButtons) { + button.classList.toggle('is-active', tool === editor.state.activeTool); + } + }; + + editor.addEventListener('change', renderState); + renderState(); + + toolbar.append(title, toolGroup, colorPicker, sizeControl, clearButton); + + return toolbar; +} diff --git a/app-v3/tsconfig.json b/app-v3/tsconfig.json new file mode 100644 index 0000000..48d633f --- /dev/null +++ b/app-v3/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true + }, + "include": ["src"] +} diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md new file mode 100644 index 0000000..95d41c9 --- /dev/null +++ b/docs/rewrite-checklist.md @@ -0,0 +1,35 @@ +# WebDraft Rewrite Checklist + +## Direction + +- Build the new editor in `app-v3/`. +- Keep the old app intact as a reference until the rewrite is useful enough to replace it. +- Use Vanilla TypeScript modules and browser APIs first. +- Avoid jQuery, React, Angular, and server-side rendering. +- Consider Web Components only for repeated UI pieces once their shape stabilizes. + +## Milestones + +- [x] Create an isolated rewrite directory. +- [x] Add a lightweight Vite + TypeScript skeleton. +- [x] Add the first canvas surface, layer manager, pencil, eraser, size control, clear command, and custom color input wrapper. +- [ ] Recreate the current tool inventory from the old app. +- [ ] Rebuild layers UI without jQuery. +- [ ] Add upload image support. +- [ ] Add PNG export. +- [ ] Add selection, copy, cut, paste. +- [ ] Add shape tools. +- [ ] Add text tool. +- [ ] Add undo/redo history. +- [ ] Add keyboard shortcuts. +- [ ] Add responsive layout polish. +- [ ] Add final smoke tests and regression tests. + +## Old Code References + +- `js/modules/WebDraft.js` - pointer flow and current editor state. +- `js/modules/Layers.js` - layer operations and previews. +- `js/modules/File.js` - upload and export behavior. +- `js/modules/Shapes.js` - rectangle and circle behavior. +- `js/modules/Text.js` - text selection and rendering behavior. +- `parts/*.part.html` - current UI inventory. From 079c20b13be1690048fdeff3a16499fcfe077f2c Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 13:43:20 +0200 Subject: [PATCH 02/62] Implement layers panel with full CRUD functionality and integrate into the toolbar. --- app-v3/src/core/layer-manager.ts | 121 ++++++++++++++++++++++++++++- app-v3/src/core/webdraft-editor.ts | 38 +++++++++ app-v3/src/styles.css | 94 ++++++++++++++++++++++ app-v3/src/ui/layers-panel.ts | 71 +++++++++++++++++ app-v3/src/ui/toolbar.ts | 3 +- docs/rewrite-checklist.md | 2 +- 6 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 app-v3/src/ui/layers-panel.ts diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 079d5da..2bb165f 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -1,14 +1,23 @@ export type Layer = { id: string; + name: string; canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; visible: boolean; }; +export type LayerSummary = { + id: string; + name: string; + active: boolean; + visible: boolean; +}; + export class LayerManager { private readonly root: HTMLElement; private readonly layers: Layer[] = []; private activeLayerId = ''; + private layerCount = 0; constructor(root: HTMLElement) { this.root = root; @@ -24,6 +33,21 @@ export class LayerManager { return layer; } + get count(): number { + return this.layers.length; + } + + get summaries(): LayerSummary[] { + return this.layers + .map((layer) => ({ + id: layer.id, + name: layer.name, + active: layer.id === this.activeLayerId, + visible: layer.visible + })) + .reverse(); + } + createLayer(width: number, height: number): Layer { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -34,15 +58,20 @@ export class LayerManager { canvas.width = width; canvas.height = height; + const id = crypto.randomUUID(); + canvas.className = 'drawing-layer'; + canvas.dataset.layerId = id; const layer: Layer = { - id: crypto.randomUUID(), + id, + name: `Layer ${this.layerCount + 1}`, canvas, context, visible: true }; + this.layerCount += 1; this.layers.push(layer); this.activeLayerId = layer.id; this.root.append(canvas); @@ -54,4 +83,94 @@ export class LayerManager { const layer = this.activeLayer; layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); } + + selectLayer(id: string): void { + this.assertLayer(id); + this.activeLayerId = id; + } + + deleteActiveLayer(): Layer | null { + if (this.layers.length <= 1) { + return null; + } + + const index = this.getActiveIndex(); + const [removedLayer] = this.layers.splice(index, 1); + removedLayer.canvas.remove(); + + const nextLayer = this.layers[index] ?? this.layers[index - 1]; + this.activeLayerId = nextLayer.id; + + return removedLayer; + } + + toggleVisibility(id: string): void { + const layer = this.assertLayer(id); + layer.visible = !layer.visible; + layer.canvas.hidden = !layer.visible; + + if (!layer.visible && layer.id === this.activeLayerId) { + const nextVisibleLayer = [...this.layers].reverse().find((item) => item.visible); + if (nextVisibleLayer) { + this.activeLayerId = nextVisibleLayer.id; + } + } + } + + moveActiveLayerUp(): boolean { + const index = this.getActiveIndex(); + + if (index === this.layers.length - 1) { + return false; + } + + this.swapLayers(index, index + 1); + return true; + } + + moveActiveLayerDown(): boolean { + const index = this.getActiveIndex(); + + if (index === 0) { + return false; + } + + this.swapLayers(index, index - 1); + return true; + } + + private getActiveIndex(): number { + const index = this.layers.findIndex((layer) => layer.id === this.activeLayerId); + + if (index < 0) { + throw new Error('No active drawing layer.'); + } + + return index; + } + + private assertLayer(id: string): Layer { + const layer = this.layers.find((item) => item.id === id); + + if (!layer) { + throw new Error(`Unknown layer: ${id}`); + } + + return layer; + } + + private swapLayers(firstIndex: number, secondIndex: number): void { + const firstLayer = this.layers[firstIndex]; + const secondLayer = this.layers[secondIndex]; + + this.layers[firstIndex] = secondLayer; + this.layers[secondIndex] = firstLayer; + this.renderLayerOrder(); + } + + private renderLayerOrder(): void { + for (const layer of this.layers) { + this.root.append(layer.canvas); + } + } } diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 778dce0..a475b1c 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,4 +1,5 @@ import { LayerManager } from './layer-manager'; +import type { LayerSummary } from './layer-manager'; import type { EditorOptions, EditorState, Point, ToolId } from './types'; export class WebDraftEditor extends EventTarget { @@ -33,6 +34,11 @@ export class WebDraftEditor extends EventTarget { this.layerManager.createLayer(this.options.width, this.options.height); this.root.append(this.eventLayer); this.bindPointerEvents(); + this.dispatchChange(); + } + + get layers(): LayerSummary[] { + return this.layerManager.summaries; } setTool(tool: ToolId): void { @@ -54,6 +60,38 @@ export class WebDraftEditor extends EventTarget { this.layerManager.clearActiveLayer(); } + addLayer(): void { + this.layerManager.createLayer(this.options.width, this.options.height); + this.dispatchChange(); + } + + deleteActiveLayer(): void { + this.layerManager.deleteActiveLayer(); + this.dispatchChange(); + } + + selectLayer(id: string): void { + this.layerManager.selectLayer(id); + this.dispatchChange(); + } + + toggleLayerVisibility(id: string): void { + this.layerManager.toggleVisibility(id); + this.dispatchChange(); + } + + moveActiveLayerUp(): void { + if (this.layerManager.moveActiveLayerUp()) { + this.dispatchChange(); + } + } + + moveActiveLayerDown(): void { + if (this.layerManager.moveActiveLayerDown()) { + this.dispatchChange(); + } + } + private bindPointerEvents(): void { this.eventLayer.addEventListener('pointerdown', (event) => { this.eventLayer.setPointerCapture(event.pointerId); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index c799070..beb0cd1 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -66,7 +66,13 @@ input { height: 100%; } +.drawing-layer { + z-index: 1; + pointer-events: none; +} + .event-layer { + z-index: 10; touch-action: none; cursor: crosshair; } @@ -157,6 +163,90 @@ input { padding: 0 12px; } +.layers-panel { + display: grid; + gap: 10px; + min-width: 0; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.panel-header h2 { + margin: 0; + color: #f4f7fb; + font-size: 13px; + font-weight: 700; +} + +.panel-actions { + display: flex; + gap: 6px; +} + +.icon-button { + display: grid; + place-items: center; + width: 28px; + height: 28px; + border: 1px solid #343941; + border-radius: 6px; + color: #f4f7fb; + background: #242832; + cursor: pointer; +} + +.icon-button:hover { + border-color: #5b9cff; +} + +.layer-list { + display: grid; + gap: 6px; +} + +.layer-item { + display: grid; + grid-template-columns: minmax(0, 1fr) 34px; + align-items: center; + min-height: 38px; + border: 1px solid #343941; + border-radius: 6px; + overflow: hidden; + background: #20242c; +} + +.layer-item.is-active { + border-color: #5b9cff; + background: #17263c; +} + +.layer-select, +.layer-visibility { + min-height: 38px; + border: 0; + color: #dce5f2; + background: transparent; + cursor: pointer; +} + +.layer-select { + min-width: 0; + padding: 0 10px; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.layer-visibility { + border-left: 1px solid #343941; +} + @media (max-width: 760px) { .app-shell { grid-template-columns: 1fr; @@ -180,4 +270,8 @@ input { .workspace { padding: 16px; } + + .layers-panel { + grid-column: 1 / -1; + } } diff --git a/app-v3/src/ui/layers-panel.ts b/app-v3/src/ui/layers-panel.ts new file mode 100644 index 0000000..49972fd --- /dev/null +++ b/app-v3/src/ui/layers-panel.ts @@ -0,0 +1,71 @@ +import type { WebDraftEditor } from '../core/webdraft-editor'; + +export function createLayersPanel(editor: WebDraftEditor): HTMLElement { + const panel = document.createElement('section'); + panel.className = 'layers-panel'; + + const header = document.createElement('div'); + header.className = 'panel-header'; + + const title = document.createElement('h2'); + title.textContent = 'Layers'; + + const actions = document.createElement('div'); + actions.className = 'panel-actions'; + + const addButton = createIconButton('Add layer', '+', () => editor.addLayer()); + const deleteButton = createIconButton('Delete layer', '-', () => editor.deleteActiveLayer()); + const upButton = createIconButton('Move layer up', '↑', () => editor.moveActiveLayerUp()); + const downButton = createIconButton('Move layer down', '↓', () => editor.moveActiveLayerDown()); + + actions.append(addButton, deleteButton, upButton, downButton); + header.append(title, actions); + + const list = document.createElement('div'); + list.className = 'layer-list'; + + const render = () => { + list.replaceChildren( + ...editor.layers.map((layer) => { + const item = document.createElement('div'); + item.className = 'layer-item'; + item.classList.toggle('is-active', layer.active); + + const selectButton = document.createElement('button'); + selectButton.type = 'button'; + selectButton.className = 'layer-select'; + selectButton.textContent = layer.name; + selectButton.addEventListener('click', () => editor.selectLayer(layer.id)); + + const visibilityButton = document.createElement('button'); + visibilityButton.type = 'button'; + visibilityButton.className = 'layer-visibility'; + visibilityButton.title = layer.visible ? 'Hide layer' : 'Show layer'; + visibilityButton.textContent = layer.visible ? '●' : '○'; + visibilityButton.addEventListener('click', () => editor.toggleLayerVisibility(layer.id)); + + item.append(selectButton, visibilityButton); + + return item; + }) + ); + }; + + editor.addEventListener('change', render); + render(); + + panel.append(header, list); + + return panel; +} + +function createIconButton(label: string, icon: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'icon-button'; + button.title = label; + button.textContent = icon; + button.addEventListener('click', onClick); + + return button; +} diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 74f65a8..d6dae18 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,6 +1,7 @@ import type { WebDraftEditor } from '../core/webdraft-editor'; import type { ToolId } from '../core/types'; import { createColorPicker } from './color-picker'; +import { createLayersPanel } from './layers-panel'; type ToolConfig = { id: ToolId; @@ -82,7 +83,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { editor.addEventListener('change', renderState); renderState(); - toolbar.append(title, toolGroup, colorPicker, sizeControl, clearButton); + toolbar.append(title, toolGroup, colorPicker, sizeControl, clearButton, createLayersPanel(editor)); return toolbar; } diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 95d41c9..8ef3c0f 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -14,7 +14,7 @@ - [x] Add a lightweight Vite + TypeScript skeleton. - [x] Add the first canvas surface, layer manager, pencil, eraser, size control, clear command, and custom color input wrapper. - [ ] Recreate the current tool inventory from the old app. -- [ ] Rebuild layers UI without jQuery. +- [x] Rebuild layers UI without jQuery. - [ ] Add upload image support. - [ ] Add PNG export. - [ ] Add selection, copy, cut, paste. From aa22765b5f8312ea5a9e4907ec46ba213c7107ef Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 16:46:23 +0200 Subject: [PATCH 03/62] Add image upload and PNG export functionality to WebDraft editor --- app-v3/src/core/layer-manager.ts | 18 +++++++ app-v3/src/core/types.ts | 14 ++++- app-v3/src/core/webdraft-editor.ts | 86 ++++++++++++++++++++++++++---- app-v3/src/styles.css | 11 ++++ app-v3/src/ui/toolbar.ts | 66 +++++++++++++++++++---- docs/rewrite-checklist.md | 4 +- 6 files changed, 174 insertions(+), 25 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 2bb165f..163d43f 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -1,3 +1,5 @@ +import {SizeWithPosition} from './types.ts'; + export type Layer = { id: string; name: string; @@ -48,6 +50,10 @@ export class LayerManager { .reverse(); } + get visibleLayers(): Layer[] { + return this.layers.filter((layer) => layer.visible); + } + createLayer(width: number, height: number): Layer { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -84,6 +90,18 @@ export class LayerManager { layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); } + drawImageOnNewLayer( + image: CanvasImageSource, + width: number, + height: number, + target: SizeWithPosition + ): Layer { + const layer = this.createLayer(width, height); + layer.context.drawImage(image, target.x, target.y, target.width, target.height); + + return layer; + } + selectLayer(id: string): void { this.assertLayer(id); this.activeLayerId = id; diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index e6c1968..7fa3318 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -1,10 +1,20 @@ -export type ToolId = 'pencil' | 'eraser'; +export enum Tool { + Pencil = 'pencil', + Eraser = 'eraser', +} export type Point = { x: number; y: number; }; +export type Size = { + width: number; + height: number; +}; + +export type SizeWithPosition = Size & Point; + export type EditorOptions = { width: number; height: number; @@ -13,7 +23,7 @@ export type EditorOptions = { }; export type EditorState = { - activeTool: ToolId; + activeTool: Tool; color: string; size: number; }; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index a475b1c..d4eaa06 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,6 +1,6 @@ -import { LayerManager } from './layer-manager'; -import type { LayerSummary } from './layer-manager'; -import type { EditorOptions, EditorState, Point, ToolId } from './types'; +import type {LayerSummary} from './layer-manager'; +import {LayerManager} from './layer-manager'; +import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; export class WebDraftEditor extends EventTarget { private readonly root: HTMLElement; @@ -22,9 +22,9 @@ export class WebDraftEditor extends EventTarget { this.eventLayer.className = 'event-layer'; this.state = { - activeTool: 'pencil', + activeTool: Tool.Pencil, color: options.color, - size: options.size + size: options.size, }; } @@ -41,7 +41,7 @@ export class WebDraftEditor extends EventTarget { return this.layerManager.summaries; } - setTool(tool: ToolId): void { + setTool(tool: Tool): void { this.state.activeTool = tool; this.dispatchChange(); } @@ -60,6 +60,40 @@ export class WebDraftEditor extends EventTarget { this.layerManager.clearActiveLayer(); } + async importImage(file: File): Promise { + const image = await this.loadImage(file); + const target = this.fitImage(image); + + this.layerManager.drawImageOnNewLayer(image, this.options.width, this.options.height, target); + this.dispatchChange(); + } + + async exportPng(): Promise { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Canvas 2D context is unavailable.'); + } + + canvas.width = this.options.width; + canvas.height = this.options.height; + + for (const layer of this.layerManager.visibleLayers) { + context.drawImage(layer.canvas, 0, 0); + } + + const blob = await new Promise((resolve) => { + canvas.toBlob(resolve, 'image/png'); + }); + + if (!blob) { + throw new Error('Unable to export PNG.'); + } + + return blob; + } + addLayer(): void { this.layerManager.createLayer(this.options.width, this.options.height); this.dispatchChange(); @@ -127,12 +161,42 @@ export class WebDraftEditor extends EventTarget { return { x: event.clientX - rect.left, - y: event.clientY - rect.top + y: event.clientY - rect.top, + }; + } + + private async loadImage(file: File): Promise { + const url = URL.createObjectURL(file); + const image = new Image(); + + try { + await new Promise((resolve, reject) => { + image.addEventListener('load', () => resolve(), {once: true}); + image.addEventListener('error', () => reject(new Error('Unable to load image.')), {once: true}); + image.src = url; + }); + + return image; + } finally { + URL.revokeObjectURL(url); + } + } + + private fitImage(image: HTMLImageElement): SizeWithPosition { + const ratio = Math.min(this.options.width / image.naturalWidth, this.options.height / image.naturalHeight, 1); + const width = image.naturalWidth * ratio; + const height = image.naturalHeight * ratio; + + return { + x: (this.options.width - width) / 2, + y: (this.options.height - height) / 2, + width, + height, }; } private drawPoint(point: Point): void { - const { context } = this.layerManager.activeLayer; + const {context} = this.layerManager.activeLayer; this.applyBrush(context); context.beginPath(); @@ -141,7 +205,7 @@ export class WebDraftEditor extends EventTarget { } private drawLine(start: Point, end: Point): void { - const { context } = this.layerManager.activeLayer; + const {context} = this.layerManager.activeLayer; this.applyBrush(context); context.beginPath(); @@ -155,7 +219,7 @@ export class WebDraftEditor extends EventTarget { context.lineJoin = 'round'; context.lineWidth = this.state.size; - if (this.state.activeTool === 'eraser') { + if (this.state.activeTool === Tool.Eraser) { context.globalCompositeOperation = 'destination-out'; context.strokeStyle = '#000000'; context.fillStyle = '#000000'; @@ -168,6 +232,6 @@ export class WebDraftEditor extends EventTarget { } private dispatchChange(): void { - this.dispatchEvent(new CustomEvent('change', { detail: this.state })); + this.dispatchEvent(new CustomEvent('change', {detail: this.state})); } } diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index beb0cd1..8a11dc8 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -20,6 +20,17 @@ input { font: inherit; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + .app-shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index d6dae18..d7a3974 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,17 +1,17 @@ -import type { WebDraftEditor } from '../core/webdraft-editor'; -import type { ToolId } from '../core/types'; -import { createColorPicker } from './color-picker'; -import { createLayersPanel } from './layers-panel'; +import {Tool} from '../core/types'; +import type {WebDraftEditor} from '../core/webdraft-editor'; +import {createColorPicker} from './color-picker'; +import {createLayersPanel} from './layers-panel'; type ToolConfig = { - id: ToolId; + id: Tool; label: string; icon: string; }; const tools: ToolConfig[] = [ - { id: 'pencil', label: 'Pencil', icon: 'P' }, - { id: 'eraser', label: 'Eraser', icon: 'E' } + {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, + {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, ]; export function createToolbar(editor: WebDraftEditor): HTMLElement { @@ -24,7 +24,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { const toolGroup = document.createElement('div'); toolGroup.className = 'toolbar__group'; - const toolButtons = new Map(); + const toolButtons = new Map(); for (const tool of tools) { const button = document.createElement('button'); @@ -65,7 +65,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { const colorPicker = createColorPicker({ label: 'Color', value: editor.state.color, - onChange: (color) => editor.setColor(color) + onChange: (color) => editor.setColor(color), }); const clearButton = document.createElement('button'); @@ -74,6 +74,42 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { clearButton.textContent = 'Clear'; clearButton.addEventListener('click', () => editor.clear()); + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.className = 'visually-hidden'; + fileInput.addEventListener('change', async () => { + const file = fileInput.files?.[0]; + + if (!file) { + return; + } + + await editor.importImage(file); + fileInput.value = ''; + }); + + const uploadButton = document.createElement('button'); + uploadButton.type = 'button'; + uploadButton.className = 'command-button'; + uploadButton.textContent = 'Upload image'; + uploadButton.addEventListener('click', () => fileInput.click()); + + const exportButton = document.createElement('button'); + exportButton.type = 'button'; + exportButton.className = 'command-button'; + exportButton.textContent = 'Export PNG'; + exportButton.addEventListener('click', async () => { + const blob = await editor.exportPng(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.download = 'webdraft-image.png'; + link.click(); + URL.revokeObjectURL(url); + }); + const renderState = () => { for (const [tool, button] of toolButtons) { button.classList.toggle('is-active', tool === editor.state.activeTool); @@ -83,7 +119,17 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { editor.addEventListener('change', renderState); renderState(); - toolbar.append(title, toolGroup, colorPicker, sizeControl, clearButton, createLayersPanel(editor)); + toolbar.append( + title, + toolGroup, + colorPicker, + sizeControl, + clearButton, + uploadButton, + exportButton, + fileInput, + createLayersPanel(editor), + ); return toolbar; } diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 8ef3c0f..c107322 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -15,8 +15,8 @@ - [x] Add the first canvas surface, layer manager, pencil, eraser, size control, clear command, and custom color input wrapper. - [ ] Recreate the current tool inventory from the old app. - [x] Rebuild layers UI without jQuery. -- [ ] Add upload image support. -- [ ] Add PNG export. +- [x] Add upload image support. +- [x] Add PNG export. - [ ] Add selection, copy, cut, paste. - [ ] Add shape tools. - [ ] Add text tool. From 9fc386b414306d3bb0de3e2f5ed7d308e29acb94 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 16:50:38 +0200 Subject: [PATCH 04/62] Add shape tools (rectangle and ellipse) with preview and commit functionality to WebDraft editor --- app-v3/src/core/types.ts | 2 + app-v3/src/core/webdraft-editor.ts | 107 +++++++++++++++++++++++++++++ app-v3/src/styles.css | 6 ++ app-v3/src/ui/toolbar.ts | 2 + docs/rewrite-checklist.md | 2 +- 5 files changed, 118 insertions(+), 1 deletion(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 7fa3318..289cd82 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -1,6 +1,8 @@ export enum Tool { Pencil = 'pencil', Eraser = 'eraser', + Rectangle = 'rectangle', + Ellipse = 'ellipse', } export type Point = { diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index d4eaa06..82dcc69 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -6,9 +6,12 @@ export class WebDraftEditor extends EventTarget { private readonly root: HTMLElement; private readonly options: EditorOptions; private readonly layerManager: LayerManager; + private readonly previewCanvas: HTMLCanvasElement; + private readonly previewContext: CanvasRenderingContext2D; private readonly eventLayer: HTMLDivElement; private isDrawing = false; private lastPoint: Point | null = null; + private shapeStartPoint: Point | null = null; readonly state: EditorState; @@ -18,9 +21,19 @@ export class WebDraftEditor extends EventTarget { this.root = root; this.options = options; this.layerManager = new LayerManager(root); + this.previewCanvas = document.createElement('canvas'); + this.previewCanvas.className = 'shape-preview-layer'; this.eventLayer = document.createElement('div'); this.eventLayer.className = 'event-layer'; + const previewContext = this.previewCanvas.getContext('2d'); + + if (!previewContext) { + throw new Error('Canvas 2D context is unavailable.'); + } + + this.previewContext = previewContext; + this.state = { activeTool: Tool.Pencil, color: options.color, @@ -31,7 +44,10 @@ export class WebDraftEditor extends EventTarget { mount(): void { this.root.style.setProperty('--canvas-width', `${this.options.width}px`); this.root.style.setProperty('--canvas-height', `${this.options.height}px`); + this.previewCanvas.width = this.options.width; + this.previewCanvas.height = this.options.height; this.layerManager.createLayer(this.options.width, this.options.height); + this.root.append(this.previewCanvas); this.root.append(this.eventLayer); this.bindPointerEvents(); this.dispatchChange(); @@ -131,6 +147,12 @@ export class WebDraftEditor extends EventTarget { this.eventLayer.setPointerCapture(event.pointerId); this.isDrawing = true; this.lastPoint = this.getPoint(event); + + if (this.isShapeTool()) { + this.shapeStartPoint = this.lastPoint; + return; + } + this.drawPoint(this.lastPoint); }); @@ -140,19 +162,32 @@ export class WebDraftEditor extends EventTarget { } const nextPoint = this.getPoint(event); + + if (this.isShapeTool()) { + this.renderShapePreview(nextPoint); + return; + } + this.drawLine(this.lastPoint, nextPoint); this.lastPoint = nextPoint; }); this.eventLayer.addEventListener('pointerup', (event) => { + if (this.isShapeTool()) { + this.commitShape(this.getPoint(event)); + } + this.eventLayer.releasePointerCapture(event.pointerId); this.isDrawing = false; this.lastPoint = null; + this.shapeStartPoint = null; }); this.eventLayer.addEventListener('pointercancel', () => { this.isDrawing = false; this.lastPoint = null; + this.shapeStartPoint = null; + this.clearShapePreview(); }); } @@ -195,6 +230,69 @@ export class WebDraftEditor extends EventTarget { }; } + private isShapeTool(): boolean { + return this.state.activeTool === Tool.Rectangle || this.state.activeTool === Tool.Ellipse; + } + + private renderShapePreview(point: Point): void { + if (!this.shapeStartPoint) { + return; + } + + this.clearShapePreview(); + this.drawShape(this.previewContext, this.getBounds(this.shapeStartPoint, point)); + } + + private commitShape(point: Point): void { + if (!this.shapeStartPoint) { + return; + } + + const {context} = this.layerManager.activeLayer; + this.drawShape(context, this.getBounds(this.shapeStartPoint, point)); + this.clearShapePreview(); + } + + private clearShapePreview(): void { + this.previewContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); + } + + private getBounds(start: Point, end: Point): SizeWithPosition { + const x = Math.min(start.x, end.x); + const y = Math.min(start.y, end.y); + const width = Math.abs(end.x - start.x); + const height = Math.abs(end.y - start.y); + + return {x, y, width, height}; + } + + private drawShape(context: CanvasRenderingContext2D, bounds: SizeWithPosition): void { + if (bounds.width < 1 || bounds.height < 1) { + return; + } + + this.applyStroke(context); + context.beginPath(); + + if (this.state.activeTool === Tool.Rectangle) { + context.rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + if (this.state.activeTool === Tool.Ellipse) { + context.ellipse( + bounds.x + bounds.width / 2, + bounds.y + bounds.height / 2, + bounds.width / 2, + bounds.height / 2, + 0, + 0, + Math.PI * 2, + ); + } + + context.stroke(); + } + private drawPoint(point: Point): void { const {context} = this.layerManager.activeLayer; this.applyBrush(context); @@ -231,6 +329,15 @@ export class WebDraftEditor extends EventTarget { context.fillStyle = this.state.color; } + private applyStroke(context: CanvasRenderingContext2D): void { + context.globalCompositeOperation = 'source-over'; + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.lineWidth = this.state.size; + context.strokeStyle = this.state.color; + context.fillStyle = this.state.color; + } + private dispatchChange(): void { this.dispatchEvent(new CustomEvent('change', {detail: this.state})); } diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 8a11dc8..9fda35d 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -70,6 +70,7 @@ input { } .drawing-layer, +.shape-preview-layer, .event-layer { position: absolute; inset: 0; @@ -82,6 +83,11 @@ input { pointer-events: none; } +.shape-preview-layer { + z-index: 5; + pointer-events: none; +} + .event-layer { z-index: 10; touch-action: none; diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index d7a3974..baef884 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -12,6 +12,8 @@ type ToolConfig = { const tools: ToolConfig[] = [ {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, + {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, + {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, ]; export function createToolbar(editor: WebDraftEditor): HTMLElement { diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index c107322..03077eb 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -18,7 +18,7 @@ - [x] Add upload image support. - [x] Add PNG export. - [ ] Add selection, copy, cut, paste. -- [ ] Add shape tools. +- [x] Add shape tools. - [ ] Add text tool. - [ ] Add undo/redo history. - [ ] Add keyboard shortcuts. From acb5c5cd274565d1c43b10445d5d642197ecceb2 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 17:55:23 +0200 Subject: [PATCH 05/62] Add keyboard shortcuts for tools, export, and editor actions with size constraints --- app-v3/src/core/webdraft-editor.ts | 2 +- app-v3/src/ui/keyboard-shortcuts.ts | 83 +++++++++++++++++++++++++++++ app-v3/src/ui/toolbar.ts | 15 +++++- docs/rewrite-checklist.md | 2 +- 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 app-v3/src/ui/keyboard-shortcuts.ts diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 82dcc69..bd74171 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -68,7 +68,7 @@ export class WebDraftEditor extends EventTarget { } setSize(size: number): void { - this.state.size = size; + this.state.size = Math.min(Math.max(size, 1), 120); this.dispatchChange(); } diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts new file mode 100644 index 0000000..0a7d708 --- /dev/null +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -0,0 +1,83 @@ +import {Tool} from '../core/types'; +import type {WebDraftEditor} from '../core/webdraft-editor'; + +type ShortcutOptions = { + openImagePicker: () => void; + exportImage: () => Promise; +}; + +export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutOptions): void { + window.addEventListener('keydown', (event) => { + if (isTypingTarget(event.target)) { + return; + } + + const key = event.key.toLowerCase(); + const isCommand = event.metaKey || event.ctrlKey; + + if (isCommand && key === 'o') { + event.preventDefault(); + options.openImagePicker(); + return; + } + + if (isCommand && key === 's') { + event.preventDefault(); + void options.exportImage(); + return; + } + + if (event.key === 'Delete' || event.key === 'Backspace') { + event.preventDefault(); + editor.clear(); + return; + } + + if (key === '[') { + event.preventDefault(); + editor.setSize(editor.state.size - 1); + return; + } + + if (key === ']') { + event.preventDefault(); + editor.setSize(editor.state.size + 1); + return; + } + + const tool = getToolForKey(key); + + if (tool) { + event.preventDefault(); + editor.setTool(tool); + } + }); +} + +function getToolForKey(key: string): Tool | null { + switch (key) { + case 'p': + return Tool.Pencil; + case 'e': + return Tool.Eraser; + case 'r': + return Tool.Rectangle; + case 'o': + return Tool.Ellipse; + default: + return null; + } +} + +function isTypingTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) { + return false; + } + + return ( + target.isContentEditable || + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement + ); +} diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index baef884..6e1af30 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,6 +1,7 @@ import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {createColorPicker} from './color-picker'; +import {bindKeyboardShortcuts} from './keyboard-shortcuts'; import {createLayersPanel} from './layers-panel'; type ToolConfig = { @@ -101,7 +102,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { exportButton.type = 'button'; exportButton.className = 'command-button'; exportButton.textContent = 'Export PNG'; - exportButton.addEventListener('click', async () => { + const exportImage = async () => { const blob = await editor.exportPng(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); @@ -110,12 +111,19 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { link.download = 'webdraft-image.png'; link.click(); URL.revokeObjectURL(url); + }; + + exportButton.addEventListener('click', () => { + void exportImage(); }); const renderState = () => { for (const [tool, button] of toolButtons) { button.classList.toggle('is-active', tool === editor.state.activeTool); } + + sizeInput.value = String(editor.state.size); + sizeValue.textContent = String(editor.state.size); }; editor.addEventListener('change', renderState); @@ -133,5 +141,10 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { createLayersPanel(editor), ); + bindKeyboardShortcuts(editor, { + openImagePicker: () => fileInput.click(), + exportImage, + }); + return toolbar; } diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 03077eb..f04c7fe 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -21,7 +21,7 @@ - [x] Add shape tools. - [ ] Add text tool. - [ ] Add undo/redo history. -- [ ] Add keyboard shortcuts. +- [x] Add keyboard shortcuts. - [ ] Add responsive layout polish. - [ ] Add final smoke tests and regression tests. From 02af02cbafe4c7a871b27683a50edc5c457941e1 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 18:00:21 +0200 Subject: [PATCH 06/62] Add undo/redo functionality with history management and integrate toolbar buttons and keyboard shortcuts --- app-v3/src/core/layer-manager.ts | 22 ++++++++++ app-v3/src/core/webdraft-editor.ts | 62 ++++++++++++++++++++++++++++- app-v3/src/styles.css | 11 +++++ app-v3/src/ui/keyboard-shortcuts.ts | 16 ++++++++ app-v3/src/ui/toolbar.ts | 16 ++++++++ docs/rewrite-checklist.md | 2 +- 6 files changed, 127 insertions(+), 2 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 163d43f..0b57b49 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -15,6 +15,11 @@ export type LayerSummary = { visible: boolean; }; +export type LayerSnapshot = { + layerId: string; + imageData: ImageData; +}; + export class LayerManager { private readonly root: HTMLElement; private readonly layers: Layer[] = []; @@ -90,6 +95,23 @@ export class LayerManager { layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); } + captureActiveLayer(): LayerSnapshot { + const layer = this.activeLayer; + + return { + layerId: layer.id, + imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height), + }; + } + + restoreLayer(snapshot: LayerSnapshot): void { + const layer = this.assertLayer(snapshot.layerId); + + layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + layer.context.putImageData(snapshot.imageData, 0, 0); + this.activeLayerId = layer.id; + } + drawImageOnNewLayer( image: CanvasImageSource, width: number, diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index bd74171..cc2f718 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,4 +1,4 @@ -import type {LayerSummary} from './layer-manager'; +import type {LayerSnapshot, LayerSummary} from './layer-manager'; import {LayerManager} from './layer-manager'; import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; @@ -12,6 +12,9 @@ export class WebDraftEditor extends EventTarget { private isDrawing = false; private lastPoint: Point | null = null; private shapeStartPoint: Point | null = null; + private pendingHistorySnapshot: LayerSnapshot | null = null; + private readonly undoStack: HistoryEntry[] = []; + private readonly redoStack: HistoryEntry[] = []; readonly state: EditorState; @@ -57,6 +60,14 @@ export class WebDraftEditor extends EventTarget { return this.layerManager.summaries; } + get canUndo(): boolean { + return this.undoStack.length > 0; + } + + get canRedo(): boolean { + return this.redoStack.length > 0; + } + setTool(tool: Tool): void { this.state.activeTool = tool; this.dispatchChange(); @@ -73,7 +84,33 @@ export class WebDraftEditor extends EventTarget { } clear(): void { + const before = this.layerManager.captureActiveLayer(); this.layerManager.clearActiveLayer(); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + } + + undo(): void { + const entry = this.undoStack.pop(); + + if (!entry) { + return; + } + + this.layerManager.restoreLayer(entry.before); + this.redoStack.push(entry); + this.dispatchChange(); + } + + redo(): void { + const entry = this.redoStack.pop(); + + if (!entry) { + return; + } + + this.layerManager.restoreLayer(entry.after); + this.undoStack.push(entry); + this.dispatchChange(); } async importImage(file: File): Promise { @@ -147,6 +184,7 @@ export class WebDraftEditor extends EventTarget { this.eventLayer.setPointerCapture(event.pointerId); this.isDrawing = true; this.lastPoint = this.getPoint(event); + this.pendingHistorySnapshot = this.layerManager.captureActiveLayer(); if (this.isShapeTool()) { this.shapeStartPoint = this.lastPoint; @@ -177,6 +215,7 @@ export class WebDraftEditor extends EventTarget { this.commitShape(this.getPoint(event)); } + this.commitPendingHistory(); this.eventLayer.releasePointerCapture(event.pointerId); this.isDrawing = false; this.lastPoint = null; @@ -187,6 +226,7 @@ export class WebDraftEditor extends EventTarget { this.isDrawing = false; this.lastPoint = null; this.shapeStartPoint = null; + this.pendingHistorySnapshot = null; this.clearShapePreview(); }); } @@ -338,7 +378,27 @@ export class WebDraftEditor extends EventTarget { context.fillStyle = this.state.color; } + private commitPendingHistory(): void { + if (!this.pendingHistorySnapshot) { + return; + } + + this.pushHistory(this.pendingHistorySnapshot, this.layerManager.captureActiveLayer()); + this.pendingHistorySnapshot = null; + } + + private pushHistory(before: LayerSnapshot, after: LayerSnapshot): void { + this.undoStack.push({before, after}); + this.redoStack.length = 0; + this.dispatchChange(); + } + private dispatchChange(): void { this.dispatchEvent(new CustomEvent('change', {detail: this.state})); } } + +type HistoryEntry = { + before: LayerSnapshot; + after: LayerSnapshot; +}; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 9fda35d..4dd3cd4 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -132,6 +132,17 @@ input { border-color: #5b9cff; } +.tool-button:disabled, +.command-button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.tool-button:disabled:hover, +.command-button:disabled:hover { + border-color: #343941; +} + .tool-button.is-active { border-color: #5b9cff; background: #173d76; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index 0a7d708..df45ddb 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -15,6 +15,22 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO const key = event.key.toLowerCase(); const isCommand = event.metaKey || event.ctrlKey; + if (isCommand && key === 'z') { + event.preventDefault(); + if (event.shiftKey) { + editor.redo(); + } else { + editor.undo(); + } + return; + } + + if (isCommand && key === 'y') { + event.preventDefault(); + editor.redo(); + return; + } + if (isCommand && key === 'o') { event.preventDefault(); options.openImagePicker(); diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 6e1af30..40d7438 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -77,6 +77,18 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { clearButton.textContent = 'Clear'; clearButton.addEventListener('click', () => editor.clear()); + const undoButton = document.createElement('button'); + undoButton.type = 'button'; + undoButton.className = 'command-button'; + undoButton.textContent = 'Undo'; + undoButton.addEventListener('click', () => editor.undo()); + + const redoButton = document.createElement('button'); + redoButton.type = 'button'; + redoButton.className = 'command-button'; + redoButton.textContent = 'Redo'; + redoButton.addEventListener('click', () => editor.redo()); + const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; @@ -124,6 +136,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeInput.value = String(editor.state.size); sizeValue.textContent = String(editor.state.size); + undoButton.disabled = !editor.canUndo; + redoButton.disabled = !editor.canRedo; }; editor.addEventListener('change', renderState); @@ -135,6 +149,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { colorPicker, sizeControl, clearButton, + undoButton, + redoButton, uploadButton, exportButton, fileInput, diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index f04c7fe..ef3bc1d 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -20,7 +20,7 @@ - [ ] Add selection, copy, cut, paste. - [x] Add shape tools. - [ ] Add text tool. -- [ ] Add undo/redo history. +- [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [ ] Add responsive layout polish. - [ ] Add final smoke tests and regression tests. From 324b352af39ac0edfbb463356f940bf3f48c09f2 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 18:06:09 +0200 Subject: [PATCH 07/62] Add selection, copy, cut, and paste functionality with toolbar buttons and keyboard shortcuts --- app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 183 +++++++++++++++++++++++++++- app-v3/src/ui/keyboard-shortcuts.ts | 20 +++ app-v3/src/ui/toolbar.ts | 25 ++++ docs/rewrite-checklist.md | 2 +- 5 files changed, 225 insertions(+), 6 deletions(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 289cd82..65952b1 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -1,4 +1,5 @@ export enum Tool { + Select = 'select', Pencil = 'pencil', Eraser = 'eraser', Rectangle = 'rectangle', diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index cc2f718..638fb21 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -12,6 +12,9 @@ export class WebDraftEditor extends EventTarget { private isDrawing = false; private lastPoint: Point | null = null; private shapeStartPoint: Point | null = null; + private selectionStartPoint: Point | null = null; + private selectionBounds: SizeWithPosition | null = null; + private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: LayerSnapshot | null = null; private readonly undoStack: HistoryEntry[] = []; private readonly redoStack: HistoryEntry[] = []; @@ -68,8 +71,19 @@ export class WebDraftEditor extends EventTarget { return this.redoStack.length > 0; } + get hasSelection(): boolean { + return this.selectionBounds !== null; + } + + get canPaste(): boolean { + return this.clipboard !== null; + } + setTool(tool: Tool): void { this.state.activeTool = tool; + if (tool !== Tool.Select) { + this.clearSelection(); + } this.dispatchChange(); } @@ -179,11 +193,85 @@ export class WebDraftEditor extends EventTarget { } } + copySelection(): void { + if (!this.selectionBounds) { + return; + } + + const bounds = this.normalizeCanvasBounds(this.selectionBounds); + + if (!bounds) { + return; + } + + const {context} = this.layerManager.activeLayer; + + this.clipboard = { + bounds, + imageData: context.getImageData(bounds.x, bounds.y, bounds.width, bounds.height), + }; + this.dispatchChange(); + } + + cutSelection(): void { + if (!this.selectionBounds) { + return; + } + + const bounds = this.normalizeCanvasBounds(this.selectionBounds); + + if (!bounds) { + return; + } + + const before = this.layerManager.captureActiveLayer(); + const {context} = this.layerManager.activeLayer; + + this.clipboard = { + bounds, + imageData: context.getImageData(bounds.x, bounds.y, bounds.width, bounds.height), + }; + context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.dispatchChange(); + } + + pasteSelection(): void { + if (!this.clipboard) { + return; + } + + const before = this.layerManager.captureActiveLayer(); + const {context} = this.layerManager.activeLayer; + const target = this.selectionBounds ?? this.clipboard.bounds; + const x = Math.round(target.x); + const y = Math.round(target.y); + + context.putImageData(this.clipboard.imageData, x, y); + this.selectionBounds = { + x, + y, + width: this.clipboard.bounds.width, + height: this.clipboard.bounds.height, + }; + this.renderSelectionFrame(); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.dispatchChange(); + } + private bindPointerEvents(): void { this.eventLayer.addEventListener('pointerdown', (event) => { this.eventLayer.setPointerCapture(event.pointerId); this.isDrawing = true; this.lastPoint = this.getPoint(event); + + if (this.state.activeTool === Tool.Select) { + this.selectionStartPoint = this.lastPoint; + this.selectionBounds = null; + this.clearPreview(); + return; + } + this.pendingHistorySnapshot = this.layerManager.captureActiveLayer(); if (this.isShapeTool()) { @@ -201,6 +289,11 @@ export class WebDraftEditor extends EventTarget { const nextPoint = this.getPoint(event); + if (this.state.activeTool === Tool.Select) { + this.renderSelectionPreview(nextPoint); + return; + } + if (this.isShapeTool()) { this.renderShapePreview(nextPoint); return; @@ -211,23 +304,31 @@ export class WebDraftEditor extends EventTarget { }); this.eventLayer.addEventListener('pointerup', (event) => { + if (this.state.activeTool === Tool.Select) { + this.commitSelection(this.getPoint(event)); + } + if (this.isShapeTool()) { this.commitShape(this.getPoint(event)); } - this.commitPendingHistory(); + if (this.state.activeTool !== Tool.Select) { + this.commitPendingHistory(); + } this.eventLayer.releasePointerCapture(event.pointerId); this.isDrawing = false; this.lastPoint = null; this.shapeStartPoint = null; + this.selectionStartPoint = null; }); this.eventLayer.addEventListener('pointercancel', () => { this.isDrawing = false; this.lastPoint = null; this.shapeStartPoint = null; + this.selectionStartPoint = null; this.pendingHistorySnapshot = null; - this.clearShapePreview(); + this.clearPreview(); }); } @@ -274,12 +375,64 @@ export class WebDraftEditor extends EventTarget { return this.state.activeTool === Tool.Rectangle || this.state.activeTool === Tool.Ellipse; } + private renderSelectionPreview(point: Point): void { + if (!this.selectionStartPoint) { + return; + } + + this.selectionBounds = this.getBounds(this.selectionStartPoint, point); + this.renderSelectionFrame(); + } + + private commitSelection(point: Point): void { + if (!this.selectionStartPoint) { + return; + } + + this.selectionBounds = this.getBounds(this.selectionStartPoint, point); + + if (!this.normalizeCanvasBounds(this.selectionBounds)) { + this.clearSelection(); + return; + } + + this.renderSelectionFrame(); + this.dispatchChange(); + } + + private clearSelection(): void { + this.selectionStartPoint = null; + this.selectionBounds = null; + this.clearPreview(); + } + + private renderSelectionFrame(): void { + if (!this.selectionBounds) { + return; + } + + const bounds = this.normalizeCanvasBounds(this.selectionBounds); + + if (!bounds) { + this.clearPreview(); + return; + } + + this.clearPreview(); + this.previewContext.save(); + this.previewContext.setLineDash([6, 4]); + this.previewContext.lineWidth = 1; + this.previewContext.strokeStyle = '#1b6cff'; + this.previewContext.strokeRect(bounds.x + 0.5, bounds.y + 0.5, bounds.width, bounds.height); + this.previewContext.restore(); + } + private renderShapePreview(point: Point): void { if (!this.shapeStartPoint) { return; } - this.clearShapePreview(); + this.clearPreview(); this.drawShape(this.previewContext, this.getBounds(this.shapeStartPoint, point)); } @@ -290,10 +443,10 @@ export class WebDraftEditor extends EventTarget { const {context} = this.layerManager.activeLayer; this.drawShape(context, this.getBounds(this.shapeStartPoint, point)); - this.clearShapePreview(); + this.clearPreview(); } - private clearShapePreview(): void { + private clearPreview(): void { this.previewContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); } @@ -306,6 +459,21 @@ export class WebDraftEditor extends EventTarget { return {x, y, width, height}; } + private normalizeCanvasBounds(bounds: SizeWithPosition): SizeWithPosition | null { + const x = Math.max(0, Math.round(bounds.x)); + const y = Math.max(0, Math.round(bounds.y)); + const right = Math.min(this.options.width, Math.round(bounds.x + bounds.width)); + const bottom = Math.min(this.options.height, Math.round(bounds.y + bounds.height)); + const width = right - x; + const height = bottom - y; + + if (width < 1 || height < 1) { + return null; + } + + return {x, y, width, height}; + } + private drawShape(context: CanvasRenderingContext2D, bounds: SizeWithPosition): void { if (bounds.width < 1 || bounds.height < 1) { return; @@ -402,3 +570,8 @@ type HistoryEntry = { before: LayerSnapshot; after: LayerSnapshot; }; + +type ClipboardSnapshot = { + bounds: SizeWithPosition; + imageData: ImageData; +}; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index df45ddb..39e1357 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -43,6 +43,24 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO return; } + if (isCommand && key === 'c') { + event.preventDefault(); + editor.copySelection(); + return; + } + + if (isCommand && key === 'x') { + event.preventDefault(); + editor.cutSelection(); + return; + } + + if (isCommand && key === 'v') { + event.preventDefault(); + editor.pasteSelection(); + return; + } + if (event.key === 'Delete' || event.key === 'Backspace') { event.preventDefault(); editor.clear(); @@ -72,6 +90,8 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO function getToolForKey(key: string): Tool | null { switch (key) { + case 's': + return Tool.Select; case 'p': return Tool.Pencil; case 'e': diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 40d7438..37817e4 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -11,6 +11,7 @@ type ToolConfig = { }; const tools: ToolConfig[] = [ + {id: Tool.Select, label: 'Select', icon: 'S'}, {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, @@ -89,6 +90,24 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { redoButton.textContent = 'Redo'; redoButton.addEventListener('click', () => editor.redo()); + const copyButton = document.createElement('button'); + copyButton.type = 'button'; + copyButton.className = 'command-button'; + copyButton.textContent = 'Copy'; + copyButton.addEventListener('click', () => editor.copySelection()); + + const cutButton = document.createElement('button'); + cutButton.type = 'button'; + cutButton.className = 'command-button'; + cutButton.textContent = 'Cut'; + cutButton.addEventListener('click', () => editor.cutSelection()); + + const pasteButton = document.createElement('button'); + pasteButton.type = 'button'; + pasteButton.className = 'command-button'; + pasteButton.textContent = 'Paste'; + pasteButton.addEventListener('click', () => editor.pasteSelection()); + const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; @@ -138,6 +157,9 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeValue.textContent = String(editor.state.size); undoButton.disabled = !editor.canUndo; redoButton.disabled = !editor.canRedo; + copyButton.disabled = !editor.hasSelection; + cutButton.disabled = !editor.hasSelection; + pasteButton.disabled = !editor.canPaste; }; editor.addEventListener('change', renderState); @@ -151,6 +173,9 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { clearButton, undoButton, redoButton, + copyButton, + cutButton, + pasteButton, uploadButton, exportButton, fileInput, diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index ef3bc1d..e8ccdbd 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -17,7 +17,7 @@ - [x] Rebuild layers UI without jQuery. - [x] Add upload image support. - [x] Add PNG export. -- [ ] Add selection, copy, cut, paste. +- [x] Add selection, copy, cut, paste. - [x] Add shape tools. - [ ] Add text tool. - [x] Add undo/redo history. From d2166866289fc2b8eab8bcd439fb2216d670e2a8 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 18:35:19 +0200 Subject: [PATCH 08/62] Add text tool with preview, input, and commit functionality; integrate into toolbar and keyboard shortcuts --- app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 182 +++++++++++++++++++++++++++- app-v3/src/styles.css | 14 +++ app-v3/src/ui/keyboard-shortcuts.ts | 2 + app-v3/src/ui/toolbar.ts | 1 + docs/rewrite-checklist.md | 2 +- 6 files changed, 200 insertions(+), 2 deletions(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 65952b1..369f9e3 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -4,6 +4,7 @@ export enum Tool { Eraser = 'eraser', Rectangle = 'rectangle', Ellipse = 'ellipse', + Text = 'text', } export type Point = { diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 638fb21..0a7d6da 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -14,6 +14,10 @@ export class WebDraftEditor extends EventTarget { private shapeStartPoint: Point | null = null; private selectionStartPoint: Point | null = null; private selectionBounds: SizeWithPosition | null = null; + private textStartPoint: Point | null = null; + private textBounds: SizeWithPosition | null = null; + private textInput: HTMLTextAreaElement | null = null; + private skipNextTextPointerDown = false; private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: LayerSnapshot | null = null; private readonly undoStack: HistoryEntry[] = []; @@ -80,6 +84,7 @@ export class WebDraftEditor extends EventTarget { } setTool(tool: Tool): void { + this.commitTextInput(); this.state.activeTool = tool; if (tool !== Tool.Select) { this.clearSelection(); @@ -272,6 +277,28 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.Text) { + if (this.textInput) { + this.commitTextInput(); + this.isDrawing = false; + this.lastPoint = null; + return; + } + + if (this.skipNextTextPointerDown) { + this.skipNextTextPointerDown = false; + this.isDrawing = false; + this.lastPoint = null; + return; + } + + this.commitTextInput(); + this.textStartPoint = this.lastPoint; + this.textBounds = null; + this.clearPreview(); + return; + } + this.pendingHistorySnapshot = this.layerManager.captureActiveLayer(); if (this.isShapeTool()) { @@ -294,6 +321,11 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.Text) { + this.renderTextPreview(nextPoint); + return; + } + if (this.isShapeTool()) { this.renderShapePreview(nextPoint); return; @@ -308,11 +340,15 @@ export class WebDraftEditor extends EventTarget { this.commitSelection(this.getPoint(event)); } + if (this.state.activeTool === Tool.Text) { + this.showTextInput(this.getPoint(event)); + } + if (this.isShapeTool()) { this.commitShape(this.getPoint(event)); } - if (this.state.activeTool !== Tool.Select) { + if (this.state.activeTool !== Tool.Select && this.state.activeTool !== Tool.Text) { this.commitPendingHistory(); } this.eventLayer.releasePointerCapture(event.pointerId); @@ -327,6 +363,7 @@ export class WebDraftEditor extends EventTarget { this.lastPoint = null; this.shapeStartPoint = null; this.selectionStartPoint = null; + this.textStartPoint = null; this.pendingHistorySnapshot = null; this.clearPreview(); }); @@ -400,6 +437,109 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + private renderTextPreview(point: Point): void { + if (!this.textStartPoint) { + return; + } + + this.textBounds = this.getBounds(this.textStartPoint, point); + this.renderTextFrame(); + } + + private showTextInput(point: Point): void { + if (!this.textStartPoint) { + return; + } + + const bounds = this.normalizeTextBounds(this.getBounds(this.textStartPoint, point)); + + this.textBounds = bounds; + this.clearPreview(); + this.createTextInput(bounds); + this.textStartPoint = null; + } + + private createTextInput(bounds: SizeWithPosition): void { + this.removeTextInput(); + + const input = document.createElement('textarea'); + input.className = 'text-input-layer'; + input.style.left = `${bounds.x}px`; + input.style.top = `${bounds.y}px`; + input.style.width = `${bounds.width}px`; + input.style.height = `${bounds.height}px`; + input.style.color = this.state.color; + input.style.font = this.getCanvasFont(); + input.placeholder = 'Text'; + + input.addEventListener('keydown', (event) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { + event.preventDefault(); + this.commitTextInput(); + } + + if (event.key === 'Escape') { + event.preventDefault(); + this.removeTextInput(); + this.clearPreview(); + } + }); + + input.addEventListener('blur', () => { + this.commitTextInput({skipNextPointerDown: true}); + }); + + this.textInput = input; + this.root.append(input); + input.focus(); + } + + private commitTextInput(options: {skipNextPointerDown?: boolean} = {}): void { + if (!this.textInput || !this.textBounds) { + return; + } + + const value = this.textInput.value.trimEnd(); + const bounds = this.textBounds; + + this.removeTextInput(); + this.clearPreview(); + this.skipNextTextPointerDown = options.skipNextPointerDown ?? false; + + if (!value.trim()) { + this.textBounds = null; + return; + } + + const before = this.layerManager.captureActiveLayer(); + const {context} = this.layerManager.activeLayer; + + this.drawText(context, value, bounds); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.textBounds = null; + } + + private removeTextInput(): void { + this.textInput?.remove(); + this.textInput = null; + } + + private renderTextFrame(): void { + if (!this.textBounds) { + return; + } + + const bounds = this.normalizeTextBounds(this.textBounds); + + this.clearPreview(); + this.previewContext.save(); + this.previewContext.setLineDash([4, 4]); + this.previewContext.lineWidth = 1; + this.previewContext.strokeStyle = '#6b9dff'; + this.previewContext.strokeRect(bounds.x + 0.5, bounds.y + 0.5, bounds.width, bounds.height); + this.previewContext.restore(); + } + private clearSelection(): void { this.selectionStartPoint = null; this.selectionBounds = null; @@ -474,6 +614,46 @@ export class WebDraftEditor extends EventTarget { return {x, y, width, height}; } + private normalizeTextBounds(bounds: SizeWithPosition): SizeWithPosition { + const x = Math.max(0, Math.round(bounds.x)); + const y = Math.max(0, Math.round(bounds.y)); + const width = Math.max(160, Math.min(this.options.width - x, Math.round(bounds.width))); + const height = Math.max(48, Math.min(this.options.height - y, Math.round(bounds.height))); + + return {x, y, width, height}; + } + + private drawText(context: CanvasRenderingContext2D, value: string, bounds: SizeWithPosition): void { + context.save(); + context.globalCompositeOperation = 'source-over'; + context.fillStyle = this.state.color; + context.font = this.getCanvasFont(); + context.textAlign = 'left'; + context.textBaseline = 'top'; + + const fontSize = this.getTextFontSize(); + const lineHeight = fontSize * 1.25; + const lines = value.split('\n'); + + lines.forEach((line, index) => { + const y = bounds.y + index * lineHeight; + + if (y + lineHeight <= bounds.y + bounds.height) { + context.fillText(line, bounds.x, y, bounds.width); + } + }); + + context.restore(); + } + + private getCanvasFont(): string { + return `${this.getTextFontSize()}px sans-serif`; + } + + private getTextFontSize(): number { + return Math.max(8, this.state.size * 2); + } + private drawShape(context: CanvasRenderingContext2D, bounds: SizeWithPosition): void { if (bounds.width < 1 || bounds.height < 1) { return; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 4dd3cd4..8afd3b0 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -94,6 +94,20 @@ input { cursor: crosshair; } +.text-input-layer { + position: absolute; + z-index: 20; + min-width: 120px; + min-height: 40px; + padding: 6px; + resize: none; + border: 1px solid #5b9cff; + border-radius: 4px; + outline: 2px solid rgb(91 156 255 / 20%); + background: rgb(255 255 255 / 82%); + caret-color: #111111; +} + .toolbar { display: grid; gap: 18px; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index 39e1357..9681516 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -100,6 +100,8 @@ function getToolForKey(key: string): Tool | null { return Tool.Rectangle; case 'o': return Tool.Ellipse; + case 't': + return Tool.Text; default: return null; } diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 37817e4..5416979 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -16,6 +16,7 @@ const tools: ToolConfig[] = [ {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, + {id: Tool.Text, label: 'Text', icon: 'T'}, ]; export function createToolbar(editor: WebDraftEditor): HTMLElement { diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index e8ccdbd..b3393f3 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -19,7 +19,7 @@ - [x] Add PNG export. - [x] Add selection, copy, cut, paste. - [x] Add shape tools. -- [ ] Add text tool. +- [x] Add text tool. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [ ] Add responsive layout polish. From 49e4fd14142443cf948c71ab4e3deaf993023645 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 18:38:36 +0200 Subject: [PATCH 09/62] Add fill options for shape tools with color picker and toggle; integrate into toolbar and editor rendering --- app-v3/src/core/types.ts | 2 ++ app-v3/src/core/webdraft-editor.ts | 18 +++++++++++++++++- app-v3/src/styles.css | 13 +++++++++++++ app-v3/src/ui/toolbar.ts | 23 +++++++++++++++++++++++ docs/rewrite-checklist.md | 1 + 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 369f9e3..cfebb36 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -29,5 +29,7 @@ export type EditorOptions = { export type EditorState = { activeTool: Tool; color: string; + fillColor: string; + fillEnabled: boolean; size: number; }; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 0a7d6da..3739b1a 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -47,6 +47,8 @@ export class WebDraftEditor extends EventTarget { this.state = { activeTool: Tool.Pencil, color: options.color, + fillColor: '#ffffff', + fillEnabled: false, size: options.size, }; } @@ -97,6 +99,16 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setFillColor(color: string): void { + this.state.fillColor = color; + this.dispatchChange(); + } + + setFillEnabled(enabled: boolean): void { + this.state.fillEnabled = enabled; + this.dispatchChange(); + } + setSize(size: number): void { this.state.size = Math.min(Math.max(size, 1), 120); this.dispatchChange(); @@ -678,6 +690,10 @@ export class WebDraftEditor extends EventTarget { ); } + if (this.state.fillEnabled) { + context.fill(); + } + context.stroke(); } @@ -723,7 +739,7 @@ export class WebDraftEditor extends EventTarget { context.lineJoin = 'round'; context.lineWidth = this.state.size; context.strokeStyle = this.state.color; - context.fillStyle = this.state.color; + context.fillStyle = this.state.fillColor; } private commitPendingHistory(): void { diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 8afd3b0..8c89fbf 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -163,6 +163,7 @@ input { } .color-picker, +.checkbox-control, .range-control { display: grid; gap: 8px; @@ -170,6 +171,18 @@ input { font-size: 13px; } +.checkbox-control { + grid-template-columns: auto 1fr; + align-items: center; + cursor: pointer; +} + +.checkbox-control input { + width: 16px; + height: 16px; + accent-color: #5b9cff; +} + .color-picker { grid-template-columns: 24px 1fr; align-items: center; diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 5416979..bc1bea5 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -73,6 +73,26 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { onChange: (color) => editor.setColor(color), }); + const fillToggle = document.createElement('label'); + fillToggle.className = 'checkbox-control'; + + const fillCheckbox = document.createElement('input'); + fillCheckbox.type = 'checkbox'; + fillCheckbox.checked = editor.state.fillEnabled; + fillCheckbox.addEventListener('change', () => { + editor.setFillEnabled(fillCheckbox.checked); + }); + + const fillText = document.createElement('span'); + fillText.textContent = 'Fill shapes'; + fillToggle.append(fillCheckbox, fillText); + + const fillColorPicker = createColorPicker({ + label: 'Fill color', + value: editor.state.fillColor, + onChange: (color) => editor.setFillColor(color), + }); + const clearButton = document.createElement('button'); clearButton.type = 'button'; clearButton.className = 'command-button'; @@ -156,6 +176,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeInput.value = String(editor.state.size); sizeValue.textContent = String(editor.state.size); + fillCheckbox.checked = editor.state.fillEnabled; undoButton.disabled = !editor.canUndo; redoButton.disabled = !editor.canRedo; copyButton.disabled = !editor.hasSelection; @@ -170,6 +191,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { title, toolGroup, colorPicker, + fillToggle, + fillColorPicker, sizeControl, clearButton, undoButton, diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index b3393f3..d7d0387 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -19,6 +19,7 @@ - [x] Add PNG export. - [x] Add selection, copy, cut, paste. - [x] Add shape tools. +- [x] Add fill options for shape tools. - [x] Add text tool. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. From 519817938405280973923aec1ce04fbc442e5d3a Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 18:57:07 +0200 Subject: [PATCH 10/62] Add color sampler tool with support for color picking, keyboard shortcut, and toolbar integration --- app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 47 +++++++++++++++++++++++++++++ app-v3/src/ui/color-picker.ts | 13 ++++++-- app-v3/src/ui/keyboard-shortcuts.ts | 2 ++ app-v3/src/ui/toolbar.ts | 3 ++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index cfebb36..d5da67d 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -2,6 +2,7 @@ export enum Tool { Select = 'select', Pencil = 'pencil', Eraser = 'eraser', + Sampler = 'sampler', Rectangle = 'rectangle', Ellipse = 'ellipse', Text = 'text', diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 3739b1a..d9d0d49 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -311,6 +311,14 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.Sampler) { + this.sampleColor(this.lastPoint); + this.setTool(Tool.Pencil); + this.isDrawing = false; + this.lastPoint = null; + return; + } + this.pendingHistorySnapshot = this.layerManager.captureActiveLayer(); if (this.isShapeTool()) { @@ -424,6 +432,37 @@ export class WebDraftEditor extends EventTarget { return this.state.activeTool === Tool.Rectangle || this.state.activeTool === Tool.Ellipse; } + private sampleColor(point: Point): void { + const x = Math.floor(point.x); + const y = Math.floor(point.y); + + if (x < 0 || y < 0 || x >= this.options.width || y >= this.options.height) { + return; + } + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Canvas 2D context is unavailable.'); + } + + canvas.width = this.options.width; + canvas.height = this.options.height; + + for (const layer of this.layerManager.visibleLayers) { + context.drawImage(layer.canvas, 0, 0); + } + + const [red, green, blue, alpha] = context.getImageData(x, y, 1, 1).data; + + if (alpha === 0) { + return; + } + + this.setColor(rgbToHex(red, green, blue)); + } + private renderSelectionPreview(point: Point): void { if (!this.selectionStartPoint) { return; @@ -771,3 +810,11 @@ type ClipboardSnapshot = { bounds: SizeWithPosition; imageData: ImageData; }; + +function rgbToHex(red: number, green: number, blue: number): string { + return `#${toHex(red)}${toHex(green)}${toHex(blue)}`; +} + +function toHex(value: number): string { + return value.toString(16).padStart(2, '0'); +} diff --git a/app-v3/src/ui/color-picker.ts b/app-v3/src/ui/color-picker.ts index ed40e48..a318fbd 100644 --- a/app-v3/src/ui/color-picker.ts +++ b/app-v3/src/ui/color-picker.ts @@ -4,7 +4,11 @@ export type ColorPickerOptions = { onChange: (color: string) => void; }; -export function createColorPicker(options: ColorPickerOptions): HTMLElement { +export type ColorPickerControl = HTMLElement & { + setValue: (color: string) => void; +}; + +export function createColorPicker(options: ColorPickerOptions): ColorPickerControl { const wrapper = document.createElement('label'); wrapper.className = 'color-picker'; @@ -25,5 +29,10 @@ export function createColorPicker(options: ColorPickerOptions): HTMLElement { wrapper.append(swatch, text, input); - return wrapper; + return Object.assign(wrapper, { + setValue(color: string) { + input.value = color; + swatch.style.backgroundColor = color; + }, + }); } diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index 9681516..f8f4171 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -96,6 +96,8 @@ function getToolForKey(key: string): Tool | null { return Tool.Pencil; case 'e': return Tool.Eraser; + case 'c': + return Tool.Sampler; case 'r': return Tool.Rectangle; case 'o': diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index bc1bea5..e981ed1 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -14,6 +14,7 @@ const tools: ToolConfig[] = [ {id: Tool.Select, label: 'Select', icon: 'S'}, {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, + {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, {id: Tool.Text, label: 'Text', icon: 'T'}, @@ -176,6 +177,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeInput.value = String(editor.state.size); sizeValue.textContent = String(editor.state.size); + colorPicker.setValue(editor.state.color); + fillColorPicker.setValue(editor.state.fillColor); fillCheckbox.checked = editor.state.fillEnabled; undoButton.disabled = !editor.canUndo; redoButton.disabled = !editor.canRedo; From 641831091c211c8fc1d017eec660a370e12558a8 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 20:19:23 +0200 Subject: [PATCH 11/62] Add web drawing tool with sensitivity control, keyboard shortcut, and toolbar integration --- app-v3/src/core/types.ts | 2 ++ app-v3/src/core/webdraft-editor.ts | 54 +++++++++++++++++++++++++++++ app-v3/src/ui/keyboard-shortcuts.ts | 2 ++ app-v3/src/ui/toolbar.ts | 24 +++++++++++++ docs/rewrite-checklist.md | 1 + 5 files changed, 83 insertions(+) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index d5da67d..58b3f82 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -3,6 +3,7 @@ export enum Tool { Pencil = 'pencil', Eraser = 'eraser', Sampler = 'sampler', + Web = 'web', Rectangle = 'rectangle', Ellipse = 'ellipse', Text = 'text', @@ -33,4 +34,5 @@ export type EditorState = { fillColor: string; fillEnabled: boolean; size: number; + webSensitivity: number; }; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index d9d0d49..cd9dff9 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -18,6 +18,7 @@ export class WebDraftEditor extends EventTarget { private textBounds: SizeWithPosition | null = null; private textInput: HTMLTextAreaElement | null = null; private skipNextTextPointerDown = false; + private webPoints: Point[] = []; private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: LayerSnapshot | null = null; private readonly undoStack: HistoryEntry[] = []; @@ -50,6 +51,7 @@ export class WebDraftEditor extends EventTarget { fillColor: '#ffffff', fillEnabled: false, size: options.size, + webSensitivity: 100, }; } @@ -114,6 +116,11 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setWebSensitivity(sensitivity: number): void { + this.state.webSensitivity = Math.min(Math.max(sensitivity, 20), 260); + this.dispatchChange(); + } + clear(): void { const before = this.layerManager.captureActiveLayer(); this.layerManager.clearActiveLayer(); @@ -326,6 +333,12 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.Web) { + this.webPoints = [this.lastPoint]; + this.drawPoint(this.lastPoint); + return; + } + this.drawPoint(this.lastPoint); }); @@ -351,6 +364,12 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.Web) { + this.drawWebLine(nextPoint); + this.lastPoint = nextPoint; + return; + } + this.drawLine(this.lastPoint, nextPoint); this.lastPoint = nextPoint; }); @@ -376,6 +395,7 @@ export class WebDraftEditor extends EventTarget { this.lastPoint = null; this.shapeStartPoint = null; this.selectionStartPoint = null; + this.webPoints = []; }); this.eventLayer.addEventListener('pointercancel', () => { @@ -384,6 +404,7 @@ export class WebDraftEditor extends EventTarget { this.shapeStartPoint = null; this.selectionStartPoint = null; this.textStartPoint = null; + this.webPoints = []; this.pendingHistorySnapshot = null; this.clearPreview(); }); @@ -755,6 +776,39 @@ export class WebDraftEditor extends EventTarget { context.stroke(); } + private drawWebLine(point: Point): void { + const {context} = this.layerManager.activeLayer; + const previousPoint = this.webPoints[this.webPoints.length - 1]; + + if (!previousPoint) { + this.webPoints.push(point); + return; + } + + this.applyBrush(context); + context.beginPath(); + context.moveTo(previousPoint.x, previousPoint.y); + context.lineTo(point.x, point.y); + context.stroke(); + + const sensitivitySquared = this.state.webSensitivity * this.state.webSensitivity; + + for (const pastPoint of this.webPoints) { + const dx = pastPoint.x - point.x; + const dy = pastPoint.y - point.y; + const distanceSquared = dx * dx + dy * dy; + + if (distanceSquared > 0 && distanceSquared < sensitivitySquared) { + context.beginPath(); + context.moveTo(point.x + dx * 0.2, point.y + dy * 0.2); + context.lineTo(pastPoint.x - dx * 0.2, pastPoint.y - dy * 0.2); + context.stroke(); + } + } + + this.webPoints.push(point); + } + private applyBrush(context: CanvasRenderingContext2D): void { context.lineCap = 'round'; context.lineJoin = 'round'; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index f8f4171..c8f9735 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -98,6 +98,8 @@ function getToolForKey(key: string): Tool | null { return Tool.Eraser; case 'c': return Tool.Sampler; + case 'w': + return Tool.Web; case 'r': return Tool.Rectangle; case 'o': diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index e981ed1..13a5d63 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -15,6 +15,7 @@ const tools: ToolConfig[] = [ {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, + {id: Tool.Web, label: 'Web', icon: 'W'}, {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, {id: Tool.Text, label: 'Text', icon: 'T'}, @@ -68,6 +69,26 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeControl.append(sizeInput, sizeValue); + const webSensitivityControl = document.createElement('label'); + webSensitivityControl.className = 'range-control'; + webSensitivityControl.textContent = 'Web'; + + const webSensitivityValue = document.createElement('span'); + webSensitivityValue.textContent = String(editor.state.webSensitivity); + + const webSensitivityInput = document.createElement('input'); + webSensitivityInput.type = 'range'; + webSensitivityInput.min = '20'; + webSensitivityInput.max = '260'; + webSensitivityInput.value = String(editor.state.webSensitivity); + webSensitivityInput.addEventListener('input', () => { + const sensitivity = Number(webSensitivityInput.value); + webSensitivityValue.textContent = String(sensitivity); + editor.setWebSensitivity(sensitivity); + }); + + webSensitivityControl.append(webSensitivityInput, webSensitivityValue); + const colorPicker = createColorPicker({ label: 'Color', value: editor.state.color, @@ -177,6 +198,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { sizeInput.value = String(editor.state.size); sizeValue.textContent = String(editor.state.size); + webSensitivityInput.value = String(editor.state.webSensitivity); + webSensitivityValue.textContent = String(editor.state.webSensitivity); colorPicker.setValue(editor.state.color); fillColorPicker.setValue(editor.state.fillColor); fillCheckbox.checked = editor.state.fillEnabled; @@ -197,6 +220,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { fillToggle, fillColorPicker, sizeControl, + webSensitivityControl, clearButton, undoButton, redoButton, diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index d7d0387..7bb410f 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -21,6 +21,7 @@ - [x] Add shape tools. - [x] Add fill options for shape tools. - [x] Add text tool. +- [x] Add web drawing tool. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [ ] Add responsive layout polish. From 6ca5399c0c0380352868cf4bebe50b0a4a55bbe2 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 20:23:51 +0200 Subject: [PATCH 12/62] Add layer renaming and preview support with UI enhancements to layers panel --- app-v3/src/core/layer-manager.ts | 11 +++++++++ app-v3/src/core/webdraft-editor.ts | 5 +++++ app-v3/src/styles.css | 36 ++++++++++++++++++++++++++---- app-v3/src/ui/layers-panel.ts | 33 ++++++++++++++++++++++----- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 0b57b49..3060e77 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -12,6 +12,7 @@ export type LayerSummary = { id: string; name: string; active: boolean; + preview: string; visible: boolean; }; @@ -50,6 +51,7 @@ export class LayerManager { id: layer.id, name: layer.name, active: layer.id === this.activeLayerId, + preview: layer.canvas.toDataURL('image/png'), visible: layer.visible })) .reverse(); @@ -129,6 +131,15 @@ export class LayerManager { this.activeLayerId = id; } + renameLayer(id: string, name: string): void { + const layer = this.assertLayer(id); + const nextName = name.trim(); + + if (nextName) { + layer.name = nextName; + } + } + deleteActiveLayer(): Layer | null { if (this.layers.length <= 1) { return null; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index cd9dff9..72ff538 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -200,6 +200,11 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + renameLayer(id: string, name: string): void { + this.layerManager.renameLayer(id, name); + this.dispatchChange(); + } + toggleLayerVisibility(id: string): void { this.layerManager.toggleVisibility(id); this.dispatchChange(); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 8c89fbf..aa2db80 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -266,9 +266,9 @@ input { .layer-item { display: grid; - grid-template-columns: minmax(0, 1fr) 34px; + grid-template-columns: 44px minmax(0, 1fr) 34px; align-items: center; - min-height: 38px; + min-height: 46px; border: 1px solid #343941; border-radius: 6px; overflow: hidden; @@ -281,21 +281,49 @@ input { } .layer-select, +.layer-preview, +.layer-name-input, .layer-visibility { - min-height: 38px; + min-height: 46px; border: 0; color: #dce5f2; background: transparent; cursor: pointer; } -.layer-select { +.layer-preview { + display: grid; + place-items: center; + padding: 5px; + border-right: 1px solid #343941; +} + +.layer-preview img { + width: 32px; + height: 32px; + object-fit: contain; + background-image: + linear-gradient(45deg, #d6dae0 25%, transparent 25%), + linear-gradient(-45deg, #d6dae0 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #d6dae0 75%), + linear-gradient(-45deg, transparent 75%, #d6dae0 75%); + background-position: 0 0, 0 4px, 4px -4px, -4px 0; + background-size: 8px 8px; +} + +.layer-name-input { min-width: 0; padding: 0 10px; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; + cursor: text; +} + +.layer-name-input:focus { + outline: 1px solid #5b9cff; + outline-offset: -1px; } .layer-visibility { diff --git a/app-v3/src/ui/layers-panel.ts b/app-v3/src/ui/layers-panel.ts index 49972fd..fc49d25 100644 --- a/app-v3/src/ui/layers-panel.ts +++ b/app-v3/src/ui/layers-panel.ts @@ -31,11 +31,32 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { item.className = 'layer-item'; item.classList.toggle('is-active', layer.active); - const selectButton = document.createElement('button'); - selectButton.type = 'button'; - selectButton.className = 'layer-select'; - selectButton.textContent = layer.name; - selectButton.addEventListener('click', () => editor.selectLayer(layer.id)); + const previewButton = document.createElement('button'); + previewButton.type = 'button'; + previewButton.className = 'layer-preview'; + previewButton.title = `Select ${layer.name}`; + previewButton.addEventListener('click', () => editor.selectLayer(layer.id)); + + const previewImage = document.createElement('img'); + previewImage.alt = ''; + previewImage.src = layer.preview; + previewButton.append(previewImage); + + const renameInput = document.createElement('input'); + renameInput.className = 'layer-name-input'; + renameInput.value = layer.name; + renameInput.title = 'Rename layer'; + renameInput.addEventListener('focus', () => { + renameInput.select(); + }); + renameInput.addEventListener('blur', () => { + editor.renameLayer(layer.id, renameInput.value); + }); + renameInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + renameInput.blur(); + } + }); const visibilityButton = document.createElement('button'); visibilityButton.type = 'button'; @@ -44,7 +65,7 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { visibilityButton.textContent = layer.visible ? '●' : '○'; visibilityButton.addEventListener('click', () => editor.toggleLayerVisibility(layer.id)); - item.append(selectButton, visibilityButton); + item.append(previewButton, renameInput, visibilityButton); return item; }) From f1b8113058676af70ab56dc2a2d17f3de8f9b9eb Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 20:41:56 +0200 Subject: [PATCH 13/62] Add active layer transformations: invert, rotate, and mirror with toolbar buttons and keyboard shortcuts --- app-v3/src/core/webdraft-editor.ts | 64 +++++++++++++++++++++++++++++ app-v3/src/styles.css | 12 ++++++ app-v3/src/ui/keyboard-shortcuts.ts | 30 ++++++++++++++ app-v3/src/ui/toolbar.ts | 27 ++++++++++++ docs/rewrite-checklist.md | 1 + 5 files changed, 134 insertions(+) diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 72ff538..8595863 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -288,6 +288,43 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + invertActiveLayer(): void { + this.commitTextInput(); + this.clearSelection(); + + const before = this.layerManager.captureActiveLayer(); + const {canvas, context} = this.layerManager.activeLayer; + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const {data} = imageData; + + for (let index = 0; index < data.length; index += 4) { + data[index] = 255 - data[index]; + data[index + 1] = 255 - data[index + 1]; + data[index + 2] = 255 - data[index + 2]; + } + + context.putImageData(imageData, 0, 0); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + } + + rotateActiveLayer(direction: 'left' | 'right'): void { + this.transformActiveLayer((context, source, width, height) => { + const angle = direction === 'left' ? -Math.PI / 2 : Math.PI / 2; + + context.translate(width / 2, height / 2); + context.rotate(angle); + context.drawImage(source, -width / 2, -height / 2); + }); + } + + mirrorActiveLayer(axis: 'horizontal' | 'vertical'): void { + this.transformActiveLayer((context, source, width, height) => { + context.translate(width / 2, height / 2); + context.scale(axis === 'horizontal' ? -1 : 1, axis === 'vertical' ? -1 : 1); + context.drawImage(source, -width / 2, -height / 2); + }); + } + private bindPointerEvents(): void { this.eventLayer.addEventListener('pointerdown', (event) => { this.eventLayer.setPointerCapture(event.pointerId); @@ -814,6 +851,33 @@ export class WebDraftEditor extends EventTarget { this.webPoints.push(point); } + private transformActiveLayer( + draw: (context: CanvasRenderingContext2D, source: HTMLCanvasElement, width: number, height: number) => void, + ): void { + this.commitTextInput(); + this.clearSelection(); + + const before = this.layerManager.captureActiveLayer(); + const {canvas, context} = this.layerManager.activeLayer; + const source = document.createElement('canvas'); + const sourceContext = source.getContext('2d'); + + if (!sourceContext) { + throw new Error('Canvas 2D context is unavailable.'); + } + + source.width = canvas.width; + source.height = canvas.height; + sourceContext.drawImage(canvas, 0, 0); + + context.clearRect(0, 0, canvas.width, canvas.height); + context.save(); + context.globalCompositeOperation = 'source-over'; + draw(context, source, canvas.width, canvas.height); + context.restore(); + this.pushHistory(before, this.layerManager.captureActiveLayer()); + } + private applyBrush(context: CanvasRenderingContext2D): void { context.lineCap = 'round'; context.lineJoin = 'round'; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index aa2db80..1aa9a72 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -218,6 +218,18 @@ input { padding: 0 12px; } +.command-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.command-grid .command-button { + min-width: 0; + padding: 0 8px; + overflow-wrap: anywhere; +} + .layers-panel { display: grid; gap: 10px; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index c8f9735..f20f7f2 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -79,6 +79,36 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO return; } + if (key === 'i') { + event.preventDefault(); + editor.invertActiveLayer(); + return; + } + + if (key === ',') { + event.preventDefault(); + editor.rotateActiveLayer('left'); + return; + } + + if (key === '.') { + event.preventDefault(); + editor.rotateActiveLayer('right'); + return; + } + + if (key === 'h') { + event.preventDefault(); + editor.mirrorActiveLayer('horizontal'); + return; + } + + if (key === 'v') { + event.preventDefault(); + editor.mirrorActiveLayer('vertical'); + return; + } + const tool = getToolForKey(key); if (tool) { diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 13a5d63..a166f37 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -151,6 +151,22 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { pasteButton.textContent = 'Paste'; pasteButton.addEventListener('click', () => editor.pasteSelection()); + const transformGroup = document.createElement('div'); + transformGroup.className = 'command-grid'; + + const invertButton = createCommandButton('Invert', () => editor.invertActiveLayer()); + const rotateLeftButton = createCommandButton('Rotate L', () => editor.rotateActiveLayer('left')); + const rotateRightButton = createCommandButton('Rotate R', () => editor.rotateActiveLayer('right')); + const mirrorHorizontalButton = createCommandButton('Mirror H', () => editor.mirrorActiveLayer('horizontal')); + const mirrorVerticalButton = createCommandButton('Mirror V', () => editor.mirrorActiveLayer('vertical')); + + invertButton.title = 'Invert colors'; + rotateLeftButton.title = 'Rotate left'; + rotateRightButton.title = 'Rotate right'; + mirrorHorizontalButton.title = 'Mirror horizontally'; + mirrorVerticalButton.title = 'Mirror vertically'; + transformGroup.append(invertButton, rotateLeftButton, rotateRightButton, mirrorHorizontalButton, mirrorVerticalButton); + const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; @@ -227,6 +243,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { copyButton, cutButton, pasteButton, + transformGroup, uploadButton, exportButton, fileInput, @@ -240,3 +257,13 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { return toolbar; } + +function createCommandButton(label: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'command-button'; + button.textContent = label; + button.addEventListener('click', onClick); + + return button; +} diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 7bb410f..e16523d 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -22,6 +22,7 @@ - [x] Add fill options for shape tools. - [x] Add text tool. - [x] Add web drawing tool. +- [x] Add active-layer invert, mirror, and rotate actions. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [ ] Add responsive layout polish. From c28902ba12517b9e1f3fb0a59d83d023766c9520 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 20:47:54 +0200 Subject: [PATCH 14/62] Add responsive layout polish and refactor toolbar structure with improved section grouping and styles --- app-v3/src/styles.css | 66 ++++++++++++++++++++++++++++++++++----- app-v3/src/ui/toolbar.ts | 38 ++++++++++++---------- docs/rewrite-checklist.md | 2 +- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 1aa9a72..b6cc24e 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -34,10 +34,17 @@ input { .app-shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); + width: 100%; + min-width: 0; min-height: 100vh; + overflow: hidden; } .sidebar { + min-width: 0; + min-height: 100vh; + max-height: 100vh; + overflow: auto; border-right: 1px solid #343941; background: #17191d; } @@ -45,6 +52,7 @@ input { .workspace { display: grid; place-items: center; + width: 100%; min-width: 0; overflow: auto; padding: 32px; @@ -110,19 +118,35 @@ input { .toolbar { display: grid; - gap: 18px; - padding: 18px; + align-content: start; + gap: 12px; + padding: 14px; } .toolbar h1 { margin: 0; + padding: 0 4px 4px; font-size: 18px; font-weight: 700; } +.toolbar__section { + display: grid; + gap: 12px; + padding: 12px; + border: 1px solid #2f343d; + border-radius: 6px; + background: #1b1e24; +} + +.toolbar__stack { + display: grid; + gap: 8px; +} + .toolbar__group { display: grid; - grid-template-columns: repeat(4, 44px); + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } @@ -137,7 +161,7 @@ input { } .tool-button { - width: 44px; + width: 100%; height: 44px; } @@ -346,20 +370,42 @@ input { .app-shell { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); + min-height: 100svh; } .sidebar { + width: 100%; + min-height: 0; + max-height: none; + overflow: hidden; border-right: 0; border-bottom: 1px solid #343941; } .toolbar { - grid-template-columns: auto auto 1fr auto; + display: flex; align-items: center; + width: 100%; + min-width: 0; + gap: 10px; + overflow-x: auto; + overflow-y: hidden; + padding: 10px; + scroll-snap-type: x proximity; + } + + .toolbar h1 { + flex: 0 0 auto; + padding: 0 8px 0 0; + } + + .toolbar__section { + flex: 0 0 min(280px, 82vw); + scroll-snap-align: start; } .toolbar__group { - grid-template-columns: repeat(2, 44px); + grid-template-columns: repeat(4, minmax(0, 1fr)); } .workspace { @@ -367,6 +413,12 @@ input { } .layers-panel { - grid-column: 1 / -1; + min-width: 0; + } +} + +@media (max-width: 480px) { + .toolbar__section { + flex-basis: min(252px, 86vw); } } diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index a166f37..04dcbda 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -229,25 +229,21 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { editor.addEventListener('change', renderState); renderState(); + const editGroup = document.createElement('div'); + editGroup.className = 'command-grid'; + editGroup.append(clearButton, undoButton, redoButton, copyButton, cutButton, pasteButton); + + const fileGroup = document.createElement('div'); + fileGroup.className = 'toolbar__stack'; + fileGroup.append(uploadButton, exportButton, fileInput); + toolbar.append( title, - toolGroup, - colorPicker, - fillToggle, - fillColorPicker, - sizeControl, - webSensitivityControl, - clearButton, - undoButton, - redoButton, - copyButton, - cutButton, - pasteButton, - transformGroup, - uploadButton, - exportButton, - fileInput, - createLayersPanel(editor), + createToolbarSection(toolGroup), + createToolbarSection(colorPicker, fillToggle, fillColorPicker, sizeControl, webSensitivityControl), + createToolbarSection(editGroup, transformGroup), + createToolbarSection(fileGroup), + createToolbarSection(createLayersPanel(editor)), ); bindKeyboardShortcuts(editor, { @@ -267,3 +263,11 @@ function createCommandButton(label: string, onClick: () => void): HTMLButtonElem return button; } + +function createToolbarSection(...children: HTMLElement[]): HTMLElement { + const section = document.createElement('section'); + section.className = 'toolbar__section'; + section.append(...children); + + return section; +} diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index e16523d..d668250 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -25,7 +25,7 @@ - [x] Add active-layer invert, mirror, and rotate actions. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. -- [ ] Add responsive layout polish. +- [x] Add responsive layout polish. - [ ] Add final smoke tests and regression tests. ## Old Code References From b643a46ed956fa7a607431b6351847a0f2ba5a53 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:03:42 +0200 Subject: [PATCH 15/62] Add core pixel buffer transformations (invert, rotate, mirror) with tests, integrate into active layer operations, and update build script with test command --- app-v3/package.json | 1 + app-v3/src/core/layer-transforms.ts | 95 ++++++++++++++++++ app-v3/src/core/webdraft-editor.ts | 75 ++++++-------- app-v3/tests/layer-transforms.test.mjs | 134 +++++++++++++++++++++++++ docs/rewrite-checklist.md | 2 +- 5 files changed, 260 insertions(+), 47 deletions(-) create mode 100644 app-v3/src/core/layer-transforms.ts create mode 100644 app-v3/tests/layer-transforms.test.mjs diff --git a/app-v3/package.json b/app-v3/package.json index 48c5b66..e5d85dd 100644 --- a/app-v3/package.json +++ b/app-v3/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", + "test": "node tests/layer-transforms.test.mjs", "build": "tsc && vite build", "preview": "vite preview --host 127.0.0.1" }, diff --git a/app-v3/src/core/layer-transforms.ts b/app-v3/src/core/layer-transforms.ts new file mode 100644 index 0000000..760febf --- /dev/null +++ b/app-v3/src/core/layer-transforms.ts @@ -0,0 +1,95 @@ +export type PixelBuffer = { + width: number; + height: number; + data: Uint8ClampedArray; +}; + +export type RotationDirection = 'left' | 'right'; +export type MirrorAxis = 'horizontal' | 'vertical'; + +export function invertPixelBuffer(buffer: PixelBuffer): PixelBuffer { + const output = clonePixelBuffer(buffer); + + for (let index = 0; index < output.data.length; index += 4) { + output.data[index] = 255 - output.data[index]; + output.data[index + 1] = 255 - output.data[index + 1]; + output.data[index + 2] = 255 - output.data[index + 2]; + } + + return output; +} + +export function rotatePixelBuffer(buffer: PixelBuffer, direction: RotationDirection): PixelBuffer { + const output = createEmptyPixelBuffer(buffer); + + for (let y = 0; y < buffer.height; y += 1) { + for (let x = 0; x < buffer.width; x += 1) { + const target = + direction === 'right' + ? {x: buffer.width - 1 - y, y: x} + : {x: y, y: buffer.height - 1 - x}; + + copyPixel(buffer, output, x, y, target.x, target.y); + } + } + + return output; +} + +export function mirrorPixelBuffer(buffer: PixelBuffer, axis: MirrorAxis): PixelBuffer { + const output = createEmptyPixelBuffer(buffer); + + for (let y = 0; y < buffer.height; y += 1) { + for (let x = 0; x < buffer.width; x += 1) { + const target = { + x: axis === 'horizontal' ? buffer.width - 1 - x : x, + y: axis === 'vertical' ? buffer.height - 1 - y : y, + }; + + copyPixel(buffer, output, x, y, target.x, target.y); + } + } + + return output; +} + +function clonePixelBuffer(buffer: PixelBuffer): PixelBuffer { + return { + width: buffer.width, + height: buffer.height, + data: new Uint8ClampedArray(buffer.data), + }; +} + +function createEmptyPixelBuffer(buffer: PixelBuffer): PixelBuffer { + return { + width: buffer.width, + height: buffer.height, + data: new Uint8ClampedArray(buffer.data.length), + }; +} + +function copyPixel( + source: PixelBuffer, + target: PixelBuffer, + sourceX: number, + sourceY: number, + targetX: number, + targetY: number, +): void { + if (targetX < 0 || targetY < 0 || targetX >= target.width || targetY >= target.height) { + return; + } + + const sourceIndex = getPixelIndex(source.width, sourceX, sourceY); + const targetIndex = getPixelIndex(target.width, targetX, targetY); + + target.data[targetIndex] = source.data[sourceIndex]; + target.data[targetIndex + 1] = source.data[sourceIndex + 1]; + target.data[targetIndex + 2] = source.data[sourceIndex + 2]; + target.data[targetIndex + 3] = source.data[sourceIndex + 3]; +} + +function getPixelIndex(width: number, x: number, y: number): number { + return (y * width + x) * 4; +} diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 8595863..0832773 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,5 +1,6 @@ import type {LayerSnapshot, LayerSummary} from './layer-manager'; import {LayerManager} from './layer-manager'; +import {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} from './layer-transforms'; import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; export class WebDraftEditor extends EventTarget { @@ -295,34 +296,36 @@ export class WebDraftEditor extends EventTarget { const before = this.layerManager.captureActiveLayer(); const {canvas, context} = this.layerManager.activeLayer; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - const {data} = imageData; + const transformed = invertPixelBuffer(imageData); - for (let index = 0; index < data.length; index += 4) { - data[index] = 255 - data[index]; - data[index + 1] = 255 - data[index + 1]; - data[index + 2] = 255 - data[index + 2]; - } - - context.putImageData(imageData, 0, 0); + context.putImageData(createImageData(transformed), 0, 0); this.pushHistory(before, this.layerManager.captureActiveLayer()); } rotateActiveLayer(direction: 'left' | 'right'): void { - this.transformActiveLayer((context, source, width, height) => { - const angle = direction === 'left' ? -Math.PI / 2 : Math.PI / 2; + this.commitTextInput(); + this.clearSelection(); - context.translate(width / 2, height / 2); - context.rotate(angle); - context.drawImage(source, -width / 2, -height / 2); - }); + const before = this.layerManager.captureActiveLayer(); + const {canvas, context} = this.layerManager.activeLayer; + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const transformed = rotatePixelBuffer(imageData, direction); + + context.putImageData(createImageData(transformed), 0, 0); + this.pushHistory(before, this.layerManager.captureActiveLayer()); } mirrorActiveLayer(axis: 'horizontal' | 'vertical'): void { - this.transformActiveLayer((context, source, width, height) => { - context.translate(width / 2, height / 2); - context.scale(axis === 'horizontal' ? -1 : 1, axis === 'vertical' ? -1 : 1); - context.drawImage(source, -width / 2, -height / 2); - }); + this.commitTextInput(); + this.clearSelection(); + + const before = this.layerManager.captureActiveLayer(); + const {canvas, context} = this.layerManager.activeLayer; + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const transformed = mirrorPixelBuffer(imageData, axis); + + context.putImageData(createImageData(transformed), 0, 0); + this.pushHistory(before, this.layerManager.captureActiveLayer()); } private bindPointerEvents(): void { @@ -851,33 +854,6 @@ export class WebDraftEditor extends EventTarget { this.webPoints.push(point); } - private transformActiveLayer( - draw: (context: CanvasRenderingContext2D, source: HTMLCanvasElement, width: number, height: number) => void, - ): void { - this.commitTextInput(); - this.clearSelection(); - - const before = this.layerManager.captureActiveLayer(); - const {canvas, context} = this.layerManager.activeLayer; - const source = document.createElement('canvas'); - const sourceContext = source.getContext('2d'); - - if (!sourceContext) { - throw new Error('Canvas 2D context is unavailable.'); - } - - source.width = canvas.width; - source.height = canvas.height; - sourceContext.drawImage(canvas, 0, 0); - - context.clearRect(0, 0, canvas.width, canvas.height); - context.save(); - context.globalCompositeOperation = 'source-over'; - draw(context, source, canvas.width, canvas.height); - context.restore(); - this.pushHistory(before, this.layerManager.captureActiveLayer()); - } - private applyBrush(context: CanvasRenderingContext2D): void { context.lineCap = 'round'; context.lineJoin = 'round'; @@ -941,3 +917,10 @@ function rgbToHex(red: number, green: number, blue: number): string { function toHex(value: number): string { return value.toString(16).padStart(2, '0'); } + +function createImageData(buffer: {data: Uint8ClampedArray; width: number; height: number}): ImageData { + const data = new Uint8ClampedArray(buffer.data.length); + data.set(buffer.data); + + return new ImageData(data, buffer.width, buffer.height); +} diff --git a/app-v3/tests/layer-transforms.test.mjs b/app-v3/tests/layer-transforms.test.mjs new file mode 100644 index 0000000..e1cbd21 --- /dev/null +++ b/app-v3/tests/layer-transforms.test.mjs @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {Buffer} from 'node:buffer'; +import ts from 'typescript'; + +const transformModule = await importTypescriptModule('../src/core/layer-transforms.ts'); +const {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} = transformModule; + +test('invertPixelBuffer inverts RGB channels and keeps alpha', () => { + const source = pixelBuffer(1, 2, [ + [10, 20, 30, 40], + [255, 128, 0, 255], + ]); + + const result = invertPixelBuffer(source); + + assertPixels(result, [ + [245, 235, 225, 40], + [0, 127, 255, 255], + ]); + assertPixels(source, [ + [10, 20, 30, 40], + [255, 128, 0, 255], + ]); +}); + +test('mirrorPixelBuffer mirrors horizontally', () => { + const source = pixelBuffer(2, 2, [ + [1, 0, 0, 255], + [2, 0, 0, 255], + [3, 0, 0, 255], + [4, 0, 0, 255], + ]); + + assertPixels(mirrorPixelBuffer(source, 'horizontal'), [ + [2, 0, 0, 255], + [1, 0, 0, 255], + [4, 0, 0, 255], + [3, 0, 0, 255], + ]); +}); + +test('mirrorPixelBuffer mirrors vertically', () => { + const source = pixelBuffer(2, 2, [ + [1, 0, 0, 255], + [2, 0, 0, 255], + [3, 0, 0, 255], + [4, 0, 0, 255], + ]); + + assertPixels(mirrorPixelBuffer(source, 'vertical'), [ + [3, 0, 0, 255], + [4, 0, 0, 255], + [1, 0, 0, 255], + [2, 0, 0, 255], + ]); +}); + +test('rotatePixelBuffer rotates square buffers right and left', () => { + const source = pixelBuffer(3, 3, [ + [1, 0, 0, 255], + [2, 0, 0, 255], + [3, 0, 0, 255], + [4, 0, 0, 255], + [5, 0, 0, 255], + [6, 0, 0, 255], + [7, 0, 0, 255], + [8, 0, 0, 255], + [9, 0, 0, 255], + ]); + + assertPixels(rotatePixelBuffer(source, 'right'), [ + [7, 0, 0, 255], + [4, 0, 0, 255], + [1, 0, 0, 255], + [8, 0, 0, 255], + [5, 0, 0, 255], + [2, 0, 0, 255], + [9, 0, 0, 255], + [6, 0, 0, 255], + [3, 0, 0, 255], + ]); + + assertPixels(rotatePixelBuffer(source, 'left'), [ + [3, 0, 0, 255], + [6, 0, 0, 255], + [9, 0, 0, 255], + [2, 0, 0, 255], + [5, 0, 0, 255], + [8, 0, 0, 255], + [1, 0, 0, 255], + [4, 0, 0, 255], + [7, 0, 0, 255], + ]); +}); + +console.log('Layer transform tests passed.'); + +async function importTypescriptModule(path) { + const url = new URL(path, import.meta.url); + const source = await readFile(url, 'utf8'); + const {outputText} = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ES2022, + target: ts.ScriptTarget.ES2022, + strict: true, + }, + fileName: url.pathname, + }); + const encoded = Buffer.from(outputText).toString('base64'); + + return import(`data:text/javascript;base64,${encoded}`); +} + +function test(name, run) { + try { + run(); + } catch (error) { + error.message = `${name}: ${error.message}`; + throw error; + } +} + +function pixelBuffer(width, height, pixels) { + return { + width, + height, + data: new Uint8ClampedArray(pixels.flat()), + }; +} + +function assertPixels(buffer, expectedPixels) { + assert.deepEqual([...buffer.data], expectedPixels.flat()); +} diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index d668250..73d6e63 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -26,7 +26,7 @@ - [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [x] Add responsive layout polish. -- [ ] Add final smoke tests and regression tests. +- [x] Add final smoke tests and regression tests. ## Old Code References From 892566fc447395e4add6e908c9687adb3a73991b Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:21:14 +0200 Subject: [PATCH 16/62] Add canvas resize, shadow effects, text formatting, and camera snap features with toolbar integration --- app-v3/src/core/layer-manager.ts | 23 +++ app-v3/src/core/types.ts | 12 ++ app-v3/src/core/webdraft-editor.ts | 216 +++++++++++++++++++++++++---- app-v3/src/styles.css | 32 ++++- app-v3/src/ui/toolbar.ts | 187 ++++++++++++++++++++++++- docs/rewrite-checklist.md | 3 +- 6 files changed, 445 insertions(+), 28 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 3060e77..1a9d26f 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -61,6 +61,10 @@ export class LayerManager { return this.layers.filter((layer) => layer.visible); } + get allLayers(): Layer[] { + return [...this.layers]; + } + createLayer(width: number, height: number): Layer { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -97,6 +101,25 @@ export class LayerManager { layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); } + resizeLayers(width: number, height: number): void { + for (const layer of this.layers) { + const source = document.createElement('canvas'); + const sourceContext = source.getContext('2d'); + + if (!sourceContext) { + throw new Error('Canvas 2D context is unavailable.'); + } + + source.width = layer.canvas.width; + source.height = layer.canvas.height; + sourceContext.drawImage(layer.canvas, 0, 0); + layer.canvas.width = width; + layer.canvas.height = height; + layer.context.clearRect(0, 0, width, height); + layer.context.drawImage(source, 0, 0); + } + } + captureActiveLayer(): LayerSnapshot { const layer = this.activeLayer; diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 58b3f82..9b569cb 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -33,6 +33,18 @@ export type EditorState = { color: string; fillColor: string; fillEnabled: boolean; + fillOpacity: number; size: number; + canvasWidth: number; + canvasHeight: number; webSensitivity: number; + shadowEnabled: boolean; + shadowColor: string; + shadowBlur: number; + shadowOffsetX: number; + shadowOffsetY: number; + textFontFamily: string; + textAlign: CanvasTextAlign; + textBold: boolean; + textItalic: boolean; }; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 0832773..a12b514 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -51,17 +51,26 @@ export class WebDraftEditor extends EventTarget { color: options.color, fillColor: '#ffffff', fillEnabled: false, + fillOpacity: 100, size: options.size, + canvasWidth: options.width, + canvasHeight: options.height, webSensitivity: 100, + shadowEnabled: false, + shadowColor: '#000000', + shadowBlur: 8, + shadowOffsetX: 8, + shadowOffsetY: 8, + textFontFamily: 'sans-serif', + textAlign: 'left', + textBold: false, + textItalic: false, }; } mount(): void { - this.root.style.setProperty('--canvas-width', `${this.options.width}px`); - this.root.style.setProperty('--canvas-height', `${this.options.height}px`); - this.previewCanvas.width = this.options.width; - this.previewCanvas.height = this.options.height; - this.layerManager.createLayer(this.options.width, this.options.height); + this.applyCanvasSize(); + this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight); this.root.append(this.previewCanvas); this.root.append(this.eventLayer); this.bindPointerEvents(); @@ -112,6 +121,11 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setFillOpacity(opacity: number): void { + this.state.fillOpacity = Math.min(Math.max(opacity, 0), 100); + this.dispatchChange(); + } + setSize(size: number): void { this.state.size = Math.min(Math.max(size, 1), 120); this.dispatchChange(); @@ -122,6 +136,72 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setShadowEnabled(enabled: boolean): void { + this.state.shadowEnabled = enabled; + this.dispatchChange(); + } + + setShadowColor(color: string): void { + this.state.shadowColor = color; + this.dispatchChange(); + } + + setShadowBlur(blur: number): void { + this.state.shadowBlur = Math.min(Math.max(blur, 0), 80); + this.dispatchChange(); + } + + setShadowOffsetX(offset: number): void { + this.state.shadowOffsetX = Math.min(Math.max(offset, -120), 120); + this.dispatchChange(); + } + + setShadowOffsetY(offset: number): void { + this.state.shadowOffsetY = Math.min(Math.max(offset, -120), 120); + this.dispatchChange(); + } + + setTextFontFamily(fontFamily: string): void { + this.state.textFontFamily = fontFamily; + this.dispatchChange(); + } + + setTextAlign(align: CanvasTextAlign): void { + this.state.textAlign = align; + this.dispatchChange(); + } + + setTextBold(enabled: boolean): void { + this.state.textBold = enabled; + this.dispatchChange(); + } + + setTextItalic(enabled: boolean): void { + this.state.textItalic = enabled; + this.dispatchChange(); + } + + resizeCanvas(width: number, height: number): void { + this.commitTextInput(); + this.clearSelection(); + + const nextWidth = Math.min(Math.max(Math.round(width), 64), 4096); + const nextHeight = Math.min(Math.max(Math.round(height), 64), 4096); + + if (nextWidth === this.state.canvasWidth && nextHeight === this.state.canvasHeight) { + return; + } + + this.state.canvasWidth = nextWidth; + this.state.canvasHeight = nextHeight; + this.layerManager.resizeLayers(nextWidth, nextHeight); + this.applyCanvasSize(); + this.undoStack.length = 0; + this.redoStack.length = 0; + this.clipboard = null; + this.dispatchChange(); + } + clear(): void { const before = this.layerManager.captureActiveLayer(); this.layerManager.clearActiveLayer(); @@ -156,10 +236,43 @@ export class WebDraftEditor extends EventTarget { const image = await this.loadImage(file); const target = this.fitImage(image); - this.layerManager.drawImageOnNewLayer(image, this.options.width, this.options.height, target); + this.layerManager.drawImageOnNewLayer(image, this.state.canvasWidth, this.state.canvasHeight, target); this.dispatchChange(); } + async importCameraFrame(): Promise { + const mediaDevices = navigator.mediaDevices; + + if (!mediaDevices?.getUserMedia) { + throw new Error('Camera is unavailable in this browser.'); + } + + const stream = await mediaDevices.getUserMedia({video: true}); + const video = document.createElement('video'); + + try { + video.muted = true; + video.playsInline = true; + video.srcObject = stream; + await video.play(); + + await new Promise((resolve) => { + if (video.videoWidth > 0 && video.videoHeight > 0) { + resolve(); + return; + } + + video.addEventListener('loadedmetadata', () => resolve(), {once: true}); + }); + + const target = this.fitImage({naturalWidth: video.videoWidth, naturalHeight: video.videoHeight}); + this.layerManager.drawImageOnNewLayer(video, this.state.canvasWidth, this.state.canvasHeight, target); + this.dispatchChange(); + } finally { + stream.getTracks().forEach((track) => track.stop()); + } + } + async exportPng(): Promise { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -168,8 +281,8 @@ export class WebDraftEditor extends EventTarget { throw new Error('Canvas 2D context is unavailable.'); } - canvas.width = this.options.width; - canvas.height = this.options.height; + canvas.width = this.state.canvasWidth; + canvas.height = this.state.canvasHeight; for (const layer of this.layerManager.visibleLayers) { context.drawImage(layer.canvas, 0, 0); @@ -187,7 +300,7 @@ export class WebDraftEditor extends EventTarget { } addLayer(): void { - this.layerManager.createLayer(this.options.width, this.options.height); + this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight); this.dispatchChange(); } @@ -481,14 +594,21 @@ export class WebDraftEditor extends EventTarget { } } - private fitImage(image: HTMLImageElement): SizeWithPosition { - const ratio = Math.min(this.options.width / image.naturalWidth, this.options.height / image.naturalHeight, 1); + private applyCanvasSize(): void { + this.root.style.setProperty('--canvas-width', `${this.state.canvasWidth}px`); + this.root.style.setProperty('--canvas-height', `${this.state.canvasHeight}px`); + this.previewCanvas.width = this.state.canvasWidth; + this.previewCanvas.height = this.state.canvasHeight; + } + + private fitImage(image: {naturalWidth: number; naturalHeight: number}): SizeWithPosition { + const ratio = Math.min(this.state.canvasWidth / image.naturalWidth, this.state.canvasHeight / image.naturalHeight, 1); const width = image.naturalWidth * ratio; const height = image.naturalHeight * ratio; return { - x: (this.options.width - width) / 2, - y: (this.options.height - height) / 2, + x: (this.state.canvasWidth - width) / 2, + y: (this.state.canvasHeight - height) / 2, width, height, }; @@ -502,7 +622,7 @@ export class WebDraftEditor extends EventTarget { const x = Math.floor(point.x); const y = Math.floor(point.y); - if (x < 0 || y < 0 || x >= this.options.width || y >= this.options.height) { + if (x < 0 || y < 0 || x >= this.state.canvasWidth || y >= this.state.canvasHeight) { return; } @@ -513,8 +633,8 @@ export class WebDraftEditor extends EventTarget { throw new Error('Canvas 2D context is unavailable.'); } - canvas.width = this.options.width; - canvas.height = this.options.height; + canvas.width = this.state.canvasWidth; + canvas.height = this.state.canvasHeight; for (const layer of this.layerManager.visibleLayers) { context.drawImage(layer.canvas, 0, 0); @@ -587,6 +707,9 @@ export class WebDraftEditor extends EventTarget { input.style.height = `${bounds.height}px`; input.style.color = this.state.color; input.style.font = this.getCanvasFont(); + input.style.fontWeight = this.state.textBold ? '700' : '400'; + input.style.fontStyle = this.state.textItalic ? 'italic' : 'normal'; + input.style.textAlign = this.state.textAlign; input.placeholder = 'Text'; input.addEventListener('keydown', (event) => { @@ -719,8 +842,8 @@ export class WebDraftEditor extends EventTarget { private normalizeCanvasBounds(bounds: SizeWithPosition): SizeWithPosition | null { const x = Math.max(0, Math.round(bounds.x)); const y = Math.max(0, Math.round(bounds.y)); - const right = Math.min(this.options.width, Math.round(bounds.x + bounds.width)); - const bottom = Math.min(this.options.height, Math.round(bounds.y + bounds.height)); + const right = Math.min(this.state.canvasWidth, Math.round(bounds.x + bounds.width)); + const bottom = Math.min(this.state.canvasHeight, Math.round(bounds.y + bounds.height)); const width = right - x; const height = bottom - y; @@ -734,8 +857,8 @@ export class WebDraftEditor extends EventTarget { private normalizeTextBounds(bounds: SizeWithPosition): SizeWithPosition { const x = Math.max(0, Math.round(bounds.x)); const y = Math.max(0, Math.round(bounds.y)); - const width = Math.max(160, Math.min(this.options.width - x, Math.round(bounds.width))); - const height = Math.max(48, Math.min(this.options.height - y, Math.round(bounds.height))); + const width = Math.max(160, Math.min(this.state.canvasWidth - x, Math.round(bounds.width))); + const height = Math.max(48, Math.min(this.state.canvasHeight - y, Math.round(bounds.height))); return {x, y, width, height}; } @@ -744,19 +867,21 @@ export class WebDraftEditor extends EventTarget { context.save(); context.globalCompositeOperation = 'source-over'; context.fillStyle = this.state.color; + this.applyShadow(context); context.font = this.getCanvasFont(); - context.textAlign = 'left'; + context.textAlign = this.state.textAlign; context.textBaseline = 'top'; const fontSize = this.getTextFontSize(); const lineHeight = fontSize * 1.25; const lines = value.split('\n'); + const x = this.getTextLineX(bounds); lines.forEach((line, index) => { const y = bounds.y + index * lineHeight; if (y + lineHeight <= bounds.y + bounds.height) { - context.fillText(line, bounds.x, y, bounds.width); + context.fillText(line, x, y, bounds.width); } }); @@ -764,7 +889,10 @@ export class WebDraftEditor extends EventTarget { } private getCanvasFont(): string { - return `${this.getTextFontSize()}px sans-serif`; + const style = this.state.textItalic ? 'italic' : 'normal'; + const weight = this.state.textBold ? '700' : '400'; + + return `${style} ${weight} ${this.getTextFontSize()}px ${this.state.textFontFamily}`; } private getTextFontSize(): number { @@ -861,6 +989,9 @@ export class WebDraftEditor extends EventTarget { if (this.state.activeTool === Tool.Eraser) { context.globalCompositeOperation = 'destination-out'; + context.shadowBlur = 0; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; context.strokeStyle = '#000000'; context.fillStyle = '#000000'; return; @@ -869,6 +1000,7 @@ export class WebDraftEditor extends EventTarget { context.globalCompositeOperation = 'source-over'; context.strokeStyle = this.state.color; context.fillStyle = this.state.color; + this.applyShadow(context); } private applyStroke(context: CanvasRenderingContext2D): void { @@ -877,7 +1009,34 @@ export class WebDraftEditor extends EventTarget { context.lineJoin = 'round'; context.lineWidth = this.state.size; context.strokeStyle = this.state.color; - context.fillStyle = this.state.fillColor; + context.fillStyle = hexToRgba(this.state.fillColor, this.state.fillOpacity / 100); + this.applyShadow(context); + } + + private applyShadow(context: CanvasRenderingContext2D): void { + if (!this.state.shadowEnabled) { + context.shadowBlur = 0; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + return; + } + + context.shadowColor = this.state.shadowColor; + context.shadowBlur = this.state.shadowBlur; + context.shadowOffsetX = this.state.shadowOffsetX; + context.shadowOffsetY = this.state.shadowOffsetY; + } + + private getTextLineX(bounds: SizeWithPosition): number { + if (this.state.textAlign === 'center') { + return bounds.x + bounds.width / 2; + } + + if (this.state.textAlign === 'right' || this.state.textAlign === 'end') { + return bounds.x + bounds.width; + } + + return bounds.x; } private commitPendingHistory(): void { @@ -918,6 +1077,15 @@ function toHex(value: number): string { return value.toString(16).padStart(2, '0'); } +function hexToRgba(hex: string, alpha: number): string { + const normalized = hex.replace('#', ''); + const red = Number.parseInt(normalized.slice(0, 2), 16); + const green = Number.parseInt(normalized.slice(2, 4), 16); + const blue = Number.parseInt(normalized.slice(4, 6), 16); + + return `rgb(${red} ${green} ${blue} / ${alpha})`; +} + function createImageData(buffer: {data: Uint8ClampedArray; width: number; height: number}): ImageData { const data = new Uint8ClampedArray(buffer.data.length); data.set(buffer.data); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index b6cc24e..3810828 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -16,7 +16,8 @@ body { } button, -input { +input, +select { font: inherit; } @@ -254,6 +255,35 @@ input { overflow-wrap: anywhere; } +.field-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.number-control, +.select-control { + min-width: 0; + min-height: 40px; + width: 100%; + border: 1px solid #343941; + border-radius: 6px; + color: #f4f7fb; + background: #242832; +} + +.number-control { + padding: 0 10px; +} + +.select-control { + padding: 0 8px; +} + +.field-grid .command-button { + grid-column: 1 / -1; +} + .layers-panel { display: grid; gap: 10px; diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 04dcbda..78e8146 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -115,6 +115,83 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { onChange: (color) => editor.setFillColor(color), }); + const fillOpacityControl = createRangeControl({ + label: 'Fill opacity', + min: 0, + max: 100, + value: editor.state.fillOpacity, + onChange: (value) => editor.setFillOpacity(value), + }); + + const shadowToggle = createCheckboxControl('Shadow', editor.state.shadowEnabled, (enabled) => { + editor.setShadowEnabled(enabled); + }); + + const shadowColorPicker = createColorPicker({ + label: 'Shadow color', + value: editor.state.shadowColor, + onChange: (color) => editor.setShadowColor(color), + }); + + const shadowBlurControl = createRangeControl({ + label: 'Shadow blur', + min: 0, + max: 80, + value: editor.state.shadowBlur, + onChange: (value) => editor.setShadowBlur(value), + }); + + const shadowXControl = createRangeControl({ + label: 'Shadow X', + min: -120, + max: 120, + value: editor.state.shadowOffsetX, + onChange: (value) => editor.setShadowOffsetX(value), + }); + + const shadowYControl = createRangeControl({ + label: 'Shadow Y', + min: -120, + max: 120, + value: editor.state.shadowOffsetY, + onChange: (value) => editor.setShadowOffsetY(value), + }); + + const fontSelect = document.createElement('select'); + fontSelect.className = 'select-control'; + fontSelect.title = 'Text font'; + for (const option of ['sans-serif', 'serif', 'monospace', 'cursive']) { + const item = document.createElement('option'); + item.value = option; + item.textContent = option; + fontSelect.append(item); + } + fontSelect.addEventListener('change', () => editor.setTextFontFamily(fontSelect.value)); + + const alignSelect = document.createElement('select'); + alignSelect.className = 'select-control'; + alignSelect.title = 'Text alignment'; + for (const option of ['left', 'center', 'right'] as CanvasTextAlign[]) { + const item = document.createElement('option'); + item.value = option; + item.textContent = option; + alignSelect.append(item); + } + alignSelect.addEventListener('change', () => editor.setTextAlign(alignSelect.value as CanvasTextAlign)); + + const textBoldToggle = createCheckboxControl('Bold', editor.state.textBold, (enabled) => { + editor.setTextBold(enabled); + }); + const textItalicToggle = createCheckboxControl('Italic', editor.state.textItalic, (enabled) => { + editor.setTextItalic(enabled); + }); + + const resizeWidthInput = createNumberInput('Width', editor.state.canvasWidth); + const resizeHeightInput = createNumberInput('Height', editor.state.canvasHeight); + const resizeButton = createCommandButton('Resize canvas', () => { + editor.resizeCanvas(Number(resizeWidthInput.value), Number(resizeHeightInput.value)); + }); + const clearButton = document.createElement('button'); clearButton.type = 'button'; clearButton.className = 'command-button'; @@ -207,6 +284,14 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { void exportImage(); }); + const cameraButton = document.createElement('button'); + cameraButton.type = 'button'; + cameraButton.className = 'command-button'; + cameraButton.textContent = 'Camera snap'; + cameraButton.addEventListener('click', () => { + void editor.importCameraFrame(); + }); + const renderState = () => { for (const [tool, button] of toolButtons) { button.classList.toggle('is-active', tool === editor.state.activeTool); @@ -218,7 +303,19 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { webSensitivityValue.textContent = String(editor.state.webSensitivity); colorPicker.setValue(editor.state.color); fillColorPicker.setValue(editor.state.fillColor); + fillOpacityControl.setValue(editor.state.fillOpacity); fillCheckbox.checked = editor.state.fillEnabled; + shadowToggle.setChecked(editor.state.shadowEnabled); + shadowColorPicker.setValue(editor.state.shadowColor); + shadowBlurControl.setValue(editor.state.shadowBlur); + shadowXControl.setValue(editor.state.shadowOffsetX); + shadowYControl.setValue(editor.state.shadowOffsetY); + fontSelect.value = editor.state.textFontFamily; + alignSelect.value = editor.state.textAlign; + textBoldToggle.setChecked(editor.state.textBold); + textItalicToggle.setChecked(editor.state.textItalic); + resizeWidthInput.value = String(editor.state.canvasWidth); + resizeHeightInput.value = String(editor.state.canvasHeight); undoButton.disabled = !editor.canUndo; redoButton.disabled = !editor.canRedo; copyButton.disabled = !editor.hasSelection; @@ -235,13 +332,24 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { const fileGroup = document.createElement('div'); fileGroup.className = 'toolbar__stack'; - fileGroup.append(uploadButton, exportButton, fileInput); + fileGroup.append(uploadButton, cameraButton, exportButton, fileInput); + + const resizeGroup = document.createElement('div'); + resizeGroup.className = 'field-grid'; + resizeGroup.append(resizeWidthInput, resizeHeightInput, resizeButton); + + const textGroup = document.createElement('div'); + textGroup.className = 'field-grid'; + textGroup.append(fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element); toolbar.append( title, createToolbarSection(toolGroup), - createToolbarSection(colorPicker, fillToggle, fillColorPicker, sizeControl, webSensitivityControl), + createToolbarSection(colorPicker, fillToggle, fillColorPicker, fillOpacityControl.element, sizeControl, webSensitivityControl), + createToolbarSection(shadowToggle.element, shadowColorPicker, shadowBlurControl.element, shadowXControl.element, shadowYControl.element), + createToolbarSection(textGroup), createToolbarSection(editGroup, transformGroup), + createToolbarSection(resizeGroup), createToolbarSection(fileGroup), createToolbarSection(createLayersPanel(editor)), ); @@ -264,6 +372,81 @@ function createCommandButton(label: string, onClick: () => void): HTMLButtonElem return button; } +function createNumberInput(label: string, value: number): HTMLInputElement { + const input = document.createElement('input'); + input.type = 'number'; + input.className = 'number-control'; + input.min = '64'; + input.max = '4096'; + input.step = '1'; + input.value = String(value); + input.placeholder = label; + input.title = label; + + return input; +} + +function createCheckboxControl( + label: string, + checked: boolean, + onChange: (checked: boolean) => void, +): {element: HTMLLabelElement; setChecked: (checked: boolean) => void} { + const element = document.createElement('label'); + element.className = 'checkbox-control'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = checked; + input.addEventListener('change', () => onChange(input.checked)); + + const text = document.createElement('span'); + text.textContent = label; + element.append(input, text); + + return { + element, + setChecked: (nextChecked: boolean) => { + input.checked = nextChecked; + }, + }; +} + +function createRangeControl(options: { + label: string; + min: number; + max: number; + value: number; + onChange: (value: number) => void; +}): {element: HTMLLabelElement; setValue: (value: number) => void} { + const element = document.createElement('label'); + element.className = 'range-control'; + element.textContent = options.label; + + const value = document.createElement('span'); + value.textContent = String(options.value); + + const input = document.createElement('input'); + input.type = 'range'; + input.min = String(options.min); + input.max = String(options.max); + input.value = String(options.value); + input.addEventListener('input', () => { + const nextValue = Number(input.value); + value.textContent = String(nextValue); + options.onChange(nextValue); + }); + + element.append(input, value); + + return { + element, + setValue: (nextValue: number) => { + input.value = String(nextValue); + value.textContent = String(nextValue); + }, + }; +} + function createToolbarSection(...children: HTMLElement[]): HTMLElement { const section = document.createElement('section'); section.className = 'toolbar__section'; diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 73d6e63..fb24e90 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -13,7 +13,7 @@ - [x] Create an isolated rewrite directory. - [x] Add a lightweight Vite + TypeScript skeleton. - [x] Add the first canvas surface, layer manager, pencil, eraser, size control, clear command, and custom color input wrapper. -- [ ] Recreate the current tool inventory from the old app. +- [x] Recreate the current tool inventory from the old app. - [x] Rebuild layers UI without jQuery. - [x] Add upload image support. - [x] Add PNG export. @@ -23,6 +23,7 @@ - [x] Add text tool. - [x] Add web drawing tool. - [x] Add active-layer invert, mirror, and rotate actions. +- [x] Add canvas resize, fill opacity, shadow controls, text options, and camera snap. - [x] Add undo/redo history. - [x] Add keyboard shortcuts. - [x] Add responsive layout polish. From 94a3dd3b6fadcaaa89c5dbdac93801f36b904bfe Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:29:13 +0200 Subject: [PATCH 17/62] Refactor: Extract canvas rendering and geometry utilities into separate modules, replace inline editor methods with reusable functions, and add unit tests for geometry helpers. Update build script to include new tests. --- app-v3/package.json | 2 +- app-v3/src/core/canvas-drawing.ts | 197 +++++++++++++++++ app-v3/src/core/editor-geometry.ts | 52 +++++ app-v3/src/core/webdraft-editor.ts | 284 ++++--------------------- app-v3/tests/editor-geometry.test.mjs | 45 ++++ app-v3/tests/layer-transforms.test.mjs | 29 +-- app-v3/tests/test-support.mjs | 28 +++ 7 files changed, 360 insertions(+), 277 deletions(-) create mode 100644 app-v3/src/core/canvas-drawing.ts create mode 100644 app-v3/src/core/editor-geometry.ts create mode 100644 app-v3/tests/editor-geometry.test.mjs create mode 100644 app-v3/tests/test-support.mjs diff --git a/app-v3/package.json b/app-v3/package.json index e5d85dd..c5e004a 100644 --- a/app-v3/package.json +++ b/app-v3/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", - "test": "node tests/layer-transforms.test.mjs", + "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs", "build": "tsc && vite build", "preview": "vite preview --host 127.0.0.1" }, diff --git a/app-v3/src/core/canvas-drawing.ts b/app-v3/src/core/canvas-drawing.ts new file mode 100644 index 0000000..b87a5e0 --- /dev/null +++ b/app-v3/src/core/canvas-drawing.ts @@ -0,0 +1,197 @@ +import type {EditorState, Point, SizeWithPosition} from './types'; +import {Tool} from './types'; + +export function drawText( + context: CanvasRenderingContext2D, + value: string, + bounds: SizeWithPosition, + state: EditorState, +): void { + context.save(); + context.globalCompositeOperation = 'source-over'; + context.fillStyle = state.color; + applyShadow(context, state); + context.font = getCanvasFont(state); + context.textAlign = state.textAlign; + context.textBaseline = 'top'; + + const fontSize = getTextFontSize(state); + const lineHeight = fontSize * 1.25; + const lines = value.split('\n'); + const x = getTextLineX(bounds, state); + + lines.forEach((line, index) => { + const y = bounds.y + index * lineHeight; + + if (y + lineHeight <= bounds.y + bounds.height) { + context.fillText(line, x, y, bounds.width); + } + }); + + context.restore(); +} + +export function getCanvasFont(state: EditorState): string { + const style = state.textItalic ? 'italic' : 'normal'; + const weight = state.textBold ? '700' : '400'; + + return `${style} ${weight} ${getTextFontSize(state)}px ${state.textFontFamily}`; +} + +export function getTextFontSize(state: EditorState): number { + return Math.max(8, state.size * 2); +} + +export function drawShape( + context: CanvasRenderingContext2D, + bounds: SizeWithPosition, + state: EditorState, +): void { + if (bounds.width < 1 || bounds.height < 1) { + return; + } + + applyStroke(context, state); + context.beginPath(); + + if (state.activeTool === Tool.Rectangle) { + context.rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + if (state.activeTool === Tool.Ellipse) { + context.ellipse( + bounds.x + bounds.width / 2, + bounds.y + bounds.height / 2, + bounds.width / 2, + bounds.height / 2, + 0, + 0, + Math.PI * 2, + ); + } + + if (state.fillEnabled) { + context.fill(); + } + + context.stroke(); +} + +export function drawPoint(context: CanvasRenderingContext2D, point: Point, state: EditorState): void { + applyBrush(context, state); + + context.beginPath(); + context.arc(point.x, point.y, state.size / 2, 0, Math.PI * 2); + context.fill(); +} + +export function drawLine(context: CanvasRenderingContext2D, start: Point, end: Point, state: EditorState): void { + applyBrush(context, state); + + context.beginPath(); + context.moveTo(start.x, start.y); + context.lineTo(end.x, end.y); + context.stroke(); +} + +export function drawWebLine( + context: CanvasRenderingContext2D, + point: Point, + points: Point[], + state: EditorState, +): Point[] { + const previousPoint = points[points.length - 1]; + + if (!previousPoint) { + return [...points, point]; + } + + applyBrush(context, state); + context.beginPath(); + context.moveTo(previousPoint.x, previousPoint.y); + context.lineTo(point.x, point.y); + context.stroke(); + + const sensitivitySquared = state.webSensitivity * state.webSensitivity; + + for (const pastPoint of points) { + const dx = pastPoint.x - point.x; + const dy = pastPoint.y - point.y; + const distanceSquared = dx * dx + dy * dy; + + if (distanceSquared > 0 && distanceSquared < sensitivitySquared) { + context.beginPath(); + context.moveTo(point.x + dx * 0.2, point.y + dy * 0.2); + context.lineTo(pastPoint.x - dx * 0.2, pastPoint.y - dy * 0.2); + context.stroke(); + } + } + + return [...points, point]; +} + +export function applyBrush(context: CanvasRenderingContext2D, state: EditorState): void { + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.lineWidth = state.size; + + if (state.activeTool === Tool.Eraser) { + context.globalCompositeOperation = 'destination-out'; + context.shadowBlur = 0; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.strokeStyle = '#000000'; + context.fillStyle = '#000000'; + return; + } + + context.globalCompositeOperation = 'source-over'; + context.strokeStyle = state.color; + context.fillStyle = state.color; + applyShadow(context, state); +} + +export function applyStroke(context: CanvasRenderingContext2D, state: EditorState): void { + context.globalCompositeOperation = 'source-over'; + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.lineWidth = state.size; + context.strokeStyle = state.color; + context.fillStyle = hexToRgba(state.fillColor, state.fillOpacity / 100); + applyShadow(context, state); +} + +export function applyShadow(context: CanvasRenderingContext2D, state: EditorState): void { + if (!state.shadowEnabled) { + context.shadowBlur = 0; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + return; + } + + context.shadowColor = state.shadowColor; + context.shadowBlur = state.shadowBlur; + context.shadowOffsetX = state.shadowOffsetX; + context.shadowOffsetY = state.shadowOffsetY; +} + +function getTextLineX(bounds: SizeWithPosition, state: EditorState): number { + if (state.textAlign === 'center') { + return bounds.x + bounds.width / 2; + } + + if (state.textAlign === 'right' || state.textAlign === 'end') { + return bounds.x + bounds.width; + } + + return bounds.x; +} + +function hexToRgba(hex: string, alpha: number): string { + const normalized = hex.replace('#', ''); + const red = Number.parseInt(normalized.slice(0, 2), 16); + const green = Number.parseInt(normalized.slice(2, 4), 16); + const blue = Number.parseInt(normalized.slice(4, 6), 16); + + return `rgb(${red} ${green} ${blue} / ${alpha})`; +} diff --git a/app-v3/src/core/editor-geometry.ts b/app-v3/src/core/editor-geometry.ts new file mode 100644 index 0000000..440dae1 --- /dev/null +++ b/app-v3/src/core/editor-geometry.ts @@ -0,0 +1,52 @@ +import type {Point, Size, SizeWithPosition} from './types'; + +export type NaturalSize = { + naturalWidth: number; + naturalHeight: number; +}; + +export function getBounds(start: Point, end: Point): SizeWithPosition { + const x = Math.min(start.x, end.x); + const y = Math.min(start.y, end.y); + const width = Math.abs(end.x - start.x); + const height = Math.abs(end.y - start.y); + + return {x, y, width, height}; +} + +export function fitNaturalSizeToCanvas(source: NaturalSize, canvasSize: Size): SizeWithPosition { + const ratio = Math.min(canvasSize.width / source.naturalWidth, canvasSize.height / source.naturalHeight, 1); + const width = source.naturalWidth * ratio; + const height = source.naturalHeight * ratio; + + return { + x: (canvasSize.width - width) / 2, + y: (canvasSize.height - height) / 2, + width, + height, + }; +} + +export function normalizeCanvasBounds(bounds: SizeWithPosition, canvasSize: Size): SizeWithPosition | null { + const x = Math.max(0, Math.round(bounds.x)); + const y = Math.max(0, Math.round(bounds.y)); + const right = Math.min(canvasSize.width, Math.round(bounds.x + bounds.width)); + const bottom = Math.min(canvasSize.height, Math.round(bounds.y + bounds.height)); + const width = right - x; + const height = bottom - y; + + if (width < 1 || height < 1) { + return null; + } + + return {x, y, width, height}; +} + +export function normalizeTextBounds(bounds: SizeWithPosition, canvasSize: Size): SizeWithPosition { + const x = Math.max(0, Math.round(bounds.x)); + const y = Math.max(0, Math.round(bounds.y)); + const width = Math.max(160, Math.min(canvasSize.width - x, Math.round(bounds.width))); + const height = Math.max(48, Math.min(canvasSize.height - y, Math.round(bounds.height))); + + return {x, y, width, height}; +} diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index a12b514..4265bfe 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,11 +1,24 @@ import type {LayerSnapshot, LayerSummary} from './layer-manager'; +import { + drawLine, + drawPoint, + drawShape, + drawText, + drawWebLine, + getCanvasFont, +} from './canvas-drawing'; +import { + fitNaturalSizeToCanvas, + getBounds, + normalizeCanvasBounds, + normalizeTextBounds, +} from './editor-geometry'; import {LayerManager} from './layer-manager'; import {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} from './layer-transforms'; import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; export class WebDraftEditor extends EventTarget { private readonly root: HTMLElement; - private readonly options: EditorOptions; private readonly layerManager: LayerManager; private readonly previewCanvas: HTMLCanvasElement; private readonly previewContext: CanvasRenderingContext2D; @@ -31,7 +44,6 @@ export class WebDraftEditor extends EventTarget { super(); this.root = root; - this.options = options; this.layerManager = new LayerManager(root); this.previewCanvas = document.createElement('canvas'); this.previewCanvas.className = 'shape-preview-layer'; @@ -234,7 +246,7 @@ export class WebDraftEditor extends EventTarget { async importImage(file: File): Promise { const image = await this.loadImage(file); - const target = this.fitImage(image); + const target = fitNaturalSizeToCanvas(image, this.canvasSize); this.layerManager.drawImageOnNewLayer(image, this.state.canvasWidth, this.state.canvasHeight, target); this.dispatchChange(); @@ -265,7 +277,7 @@ export class WebDraftEditor extends EventTarget { video.addEventListener('loadedmetadata', () => resolve(), {once: true}); }); - const target = this.fitImage({naturalWidth: video.videoWidth, naturalHeight: video.videoHeight}); + const target = fitNaturalSizeToCanvas({naturalWidth: video.videoWidth, naturalHeight: video.videoHeight}, this.canvasSize); this.layerManager.drawImageOnNewLayer(video, this.state.canvasWidth, this.state.canvasHeight, target); this.dispatchChange(); } finally { @@ -341,7 +353,7 @@ export class WebDraftEditor extends EventTarget { return; } - const bounds = this.normalizeCanvasBounds(this.selectionBounds); + const bounds = normalizeCanvasBounds(this.selectionBounds, this.canvasSize); if (!bounds) { return; @@ -361,7 +373,7 @@ export class WebDraftEditor extends EventTarget { return; } - const bounds = this.normalizeCanvasBounds(this.selectionBounds); + const bounds = normalizeCanvasBounds(this.selectionBounds, this.canvasSize); if (!bounds) { return; @@ -493,11 +505,11 @@ export class WebDraftEditor extends EventTarget { if (this.state.activeTool === Tool.Web) { this.webPoints = [this.lastPoint]; - this.drawPoint(this.lastPoint); + drawPoint(this.layerManager.activeLayer.context, this.lastPoint, this.state); return; } - this.drawPoint(this.lastPoint); + drawPoint(this.layerManager.activeLayer.context, this.lastPoint, this.state); }); this.eventLayer.addEventListener('pointermove', (event) => { @@ -523,12 +535,12 @@ export class WebDraftEditor extends EventTarget { } if (this.state.activeTool === Tool.Web) { - this.drawWebLine(nextPoint); + this.webPoints = drawWebLine(this.layerManager.activeLayer.context, nextPoint, this.webPoints, this.state); this.lastPoint = nextPoint; return; } - this.drawLine(this.lastPoint, nextPoint); + drawLine(this.layerManager.activeLayer.context, this.lastPoint, nextPoint, this.state); this.lastPoint = nextPoint; }); @@ -601,16 +613,10 @@ export class WebDraftEditor extends EventTarget { this.previewCanvas.height = this.state.canvasHeight; } - private fitImage(image: {naturalWidth: number; naturalHeight: number}): SizeWithPosition { - const ratio = Math.min(this.state.canvasWidth / image.naturalWidth, this.state.canvasHeight / image.naturalHeight, 1); - const width = image.naturalWidth * ratio; - const height = image.naturalHeight * ratio; - + private get canvasSize(): {width: number; height: number} { return { - x: (this.state.canvasWidth - width) / 2, - y: (this.state.canvasHeight - height) / 2, - width, - height, + width: this.state.canvasWidth, + height: this.state.canvasHeight, }; } @@ -654,7 +660,7 @@ export class WebDraftEditor extends EventTarget { return; } - this.selectionBounds = this.getBounds(this.selectionStartPoint, point); + this.selectionBounds = getBounds(this.selectionStartPoint, point); this.renderSelectionFrame(); } @@ -663,9 +669,9 @@ export class WebDraftEditor extends EventTarget { return; } - this.selectionBounds = this.getBounds(this.selectionStartPoint, point); + this.selectionBounds = getBounds(this.selectionStartPoint, point); - if (!this.normalizeCanvasBounds(this.selectionBounds)) { + if (!normalizeCanvasBounds(this.selectionBounds, this.canvasSize)) { this.clearSelection(); return; } @@ -679,7 +685,7 @@ export class WebDraftEditor extends EventTarget { return; } - this.textBounds = this.getBounds(this.textStartPoint, point); + this.textBounds = getBounds(this.textStartPoint, point); this.renderTextFrame(); } @@ -688,7 +694,7 @@ export class WebDraftEditor extends EventTarget { return; } - const bounds = this.normalizeTextBounds(this.getBounds(this.textStartPoint, point)); + const bounds = normalizeTextBounds(getBounds(this.textStartPoint, point), this.canvasSize); this.textBounds = bounds; this.clearPreview(); @@ -706,7 +712,7 @@ export class WebDraftEditor extends EventTarget { input.style.width = `${bounds.width}px`; input.style.height = `${bounds.height}px`; input.style.color = this.state.color; - input.style.font = this.getCanvasFont(); + input.style.font = getCanvasFont(this.state); input.style.fontWeight = this.state.textBold ? '700' : '400'; input.style.fontStyle = this.state.textItalic ? 'italic' : 'normal'; input.style.textAlign = this.state.textAlign; @@ -754,7 +760,7 @@ export class WebDraftEditor extends EventTarget { const before = this.layerManager.captureActiveLayer(); const {context} = this.layerManager.activeLayer; - this.drawText(context, value, bounds); + drawText(context, value, bounds, this.state); this.pushHistory(before, this.layerManager.captureActiveLayer()); this.textBounds = null; } @@ -769,7 +775,7 @@ export class WebDraftEditor extends EventTarget { return; } - const bounds = this.normalizeTextBounds(this.textBounds); + const bounds = normalizeTextBounds(this.textBounds, this.canvasSize); this.clearPreview(); this.previewContext.save(); @@ -791,7 +797,7 @@ export class WebDraftEditor extends EventTarget { return; } - const bounds = this.normalizeCanvasBounds(this.selectionBounds); + const bounds = normalizeCanvasBounds(this.selectionBounds, this.canvasSize); if (!bounds) { this.clearPreview(); @@ -813,7 +819,7 @@ export class WebDraftEditor extends EventTarget { } this.clearPreview(); - this.drawShape(this.previewContext, this.getBounds(this.shapeStartPoint, point)); + drawShape(this.previewContext, getBounds(this.shapeStartPoint, point), this.state); } private commitShape(point: Point): void { @@ -822,7 +828,7 @@ export class WebDraftEditor extends EventTarget { } const {context} = this.layerManager.activeLayer; - this.drawShape(context, this.getBounds(this.shapeStartPoint, point)); + drawShape(context, getBounds(this.shapeStartPoint, point), this.state); this.clearPreview(); } @@ -830,215 +836,6 @@ export class WebDraftEditor extends EventTarget { this.previewContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); } - private getBounds(start: Point, end: Point): SizeWithPosition { - const x = Math.min(start.x, end.x); - const y = Math.min(start.y, end.y); - const width = Math.abs(end.x - start.x); - const height = Math.abs(end.y - start.y); - - return {x, y, width, height}; - } - - private normalizeCanvasBounds(bounds: SizeWithPosition): SizeWithPosition | null { - const x = Math.max(0, Math.round(bounds.x)); - const y = Math.max(0, Math.round(bounds.y)); - const right = Math.min(this.state.canvasWidth, Math.round(bounds.x + bounds.width)); - const bottom = Math.min(this.state.canvasHeight, Math.round(bounds.y + bounds.height)); - const width = right - x; - const height = bottom - y; - - if (width < 1 || height < 1) { - return null; - } - - return {x, y, width, height}; - } - - private normalizeTextBounds(bounds: SizeWithPosition): SizeWithPosition { - const x = Math.max(0, Math.round(bounds.x)); - const y = Math.max(0, Math.round(bounds.y)); - const width = Math.max(160, Math.min(this.state.canvasWidth - x, Math.round(bounds.width))); - const height = Math.max(48, Math.min(this.state.canvasHeight - y, Math.round(bounds.height))); - - return {x, y, width, height}; - } - - private drawText(context: CanvasRenderingContext2D, value: string, bounds: SizeWithPosition): void { - context.save(); - context.globalCompositeOperation = 'source-over'; - context.fillStyle = this.state.color; - this.applyShadow(context); - context.font = this.getCanvasFont(); - context.textAlign = this.state.textAlign; - context.textBaseline = 'top'; - - const fontSize = this.getTextFontSize(); - const lineHeight = fontSize * 1.25; - const lines = value.split('\n'); - const x = this.getTextLineX(bounds); - - lines.forEach((line, index) => { - const y = bounds.y + index * lineHeight; - - if (y + lineHeight <= bounds.y + bounds.height) { - context.fillText(line, x, y, bounds.width); - } - }); - - context.restore(); - } - - private getCanvasFont(): string { - const style = this.state.textItalic ? 'italic' : 'normal'; - const weight = this.state.textBold ? '700' : '400'; - - return `${style} ${weight} ${this.getTextFontSize()}px ${this.state.textFontFamily}`; - } - - private getTextFontSize(): number { - return Math.max(8, this.state.size * 2); - } - - private drawShape(context: CanvasRenderingContext2D, bounds: SizeWithPosition): void { - if (bounds.width < 1 || bounds.height < 1) { - return; - } - - this.applyStroke(context); - context.beginPath(); - - if (this.state.activeTool === Tool.Rectangle) { - context.rect(bounds.x, bounds.y, bounds.width, bounds.height); - } - - if (this.state.activeTool === Tool.Ellipse) { - context.ellipse( - bounds.x + bounds.width / 2, - bounds.y + bounds.height / 2, - bounds.width / 2, - bounds.height / 2, - 0, - 0, - Math.PI * 2, - ); - } - - if (this.state.fillEnabled) { - context.fill(); - } - - context.stroke(); - } - - private drawPoint(point: Point): void { - const {context} = this.layerManager.activeLayer; - this.applyBrush(context); - - context.beginPath(); - context.arc(point.x, point.y, this.state.size / 2, 0, Math.PI * 2); - context.fill(); - } - - private drawLine(start: Point, end: Point): void { - const {context} = this.layerManager.activeLayer; - this.applyBrush(context); - - context.beginPath(); - context.moveTo(start.x, start.y); - context.lineTo(end.x, end.y); - context.stroke(); - } - - private drawWebLine(point: Point): void { - const {context} = this.layerManager.activeLayer; - const previousPoint = this.webPoints[this.webPoints.length - 1]; - - if (!previousPoint) { - this.webPoints.push(point); - return; - } - - this.applyBrush(context); - context.beginPath(); - context.moveTo(previousPoint.x, previousPoint.y); - context.lineTo(point.x, point.y); - context.stroke(); - - const sensitivitySquared = this.state.webSensitivity * this.state.webSensitivity; - - for (const pastPoint of this.webPoints) { - const dx = pastPoint.x - point.x; - const dy = pastPoint.y - point.y; - const distanceSquared = dx * dx + dy * dy; - - if (distanceSquared > 0 && distanceSquared < sensitivitySquared) { - context.beginPath(); - context.moveTo(point.x + dx * 0.2, point.y + dy * 0.2); - context.lineTo(pastPoint.x - dx * 0.2, pastPoint.y - dy * 0.2); - context.stroke(); - } - } - - this.webPoints.push(point); - } - - private applyBrush(context: CanvasRenderingContext2D): void { - context.lineCap = 'round'; - context.lineJoin = 'round'; - context.lineWidth = this.state.size; - - if (this.state.activeTool === Tool.Eraser) { - context.globalCompositeOperation = 'destination-out'; - context.shadowBlur = 0; - context.shadowOffsetX = 0; - context.shadowOffsetY = 0; - context.strokeStyle = '#000000'; - context.fillStyle = '#000000'; - return; - } - - context.globalCompositeOperation = 'source-over'; - context.strokeStyle = this.state.color; - context.fillStyle = this.state.color; - this.applyShadow(context); - } - - private applyStroke(context: CanvasRenderingContext2D): void { - context.globalCompositeOperation = 'source-over'; - context.lineCap = 'round'; - context.lineJoin = 'round'; - context.lineWidth = this.state.size; - context.strokeStyle = this.state.color; - context.fillStyle = hexToRgba(this.state.fillColor, this.state.fillOpacity / 100); - this.applyShadow(context); - } - - private applyShadow(context: CanvasRenderingContext2D): void { - if (!this.state.shadowEnabled) { - context.shadowBlur = 0; - context.shadowOffsetX = 0; - context.shadowOffsetY = 0; - return; - } - - context.shadowColor = this.state.shadowColor; - context.shadowBlur = this.state.shadowBlur; - context.shadowOffsetX = this.state.shadowOffsetX; - context.shadowOffsetY = this.state.shadowOffsetY; - } - - private getTextLineX(bounds: SizeWithPosition): number { - if (this.state.textAlign === 'center') { - return bounds.x + bounds.width / 2; - } - - if (this.state.textAlign === 'right' || this.state.textAlign === 'end') { - return bounds.x + bounds.width; - } - - return bounds.x; - } - private commitPendingHistory(): void { if (!this.pendingHistorySnapshot) { return; @@ -1077,15 +874,6 @@ function toHex(value: number): string { return value.toString(16).padStart(2, '0'); } -function hexToRgba(hex: string, alpha: number): string { - const normalized = hex.replace('#', ''); - const red = Number.parseInt(normalized.slice(0, 2), 16); - const green = Number.parseInt(normalized.slice(2, 4), 16); - const blue = Number.parseInt(normalized.slice(4, 6), 16); - - return `rgb(${red} ${green} ${blue} / ${alpha})`; -} - function createImageData(buffer: {data: Uint8ClampedArray; width: number; height: number}): ImageData { const data = new Uint8ClampedArray(buffer.data.length); data.set(buffer.data); diff --git a/app-v3/tests/editor-geometry.test.mjs b/app-v3/tests/editor-geometry.test.mjs new file mode 100644 index 0000000..521c8b1 --- /dev/null +++ b/app-v3/tests/editor-geometry.test.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import {importTypescriptModule, test} from './test-support.mjs'; + +const geometryModule = await importTypescriptModule('../src/core/editor-geometry.ts'); +const {fitNaturalSizeToCanvas, getBounds, normalizeCanvasBounds, normalizeTextBounds} = geometryModule; + +test('getBounds normalizes drag direction', () => { + assert.deepEqual(getBounds({x: 80, y: 90}, {x: 20, y: 10}), { + x: 20, + y: 10, + width: 60, + height: 80, + }); +}); + +test('normalizeCanvasBounds clips selection to the canvas', () => { + assert.deepEqual(normalizeCanvasBounds({x: -10, y: 5, width: 50, height: 110}, {width: 100, height: 80}), { + x: 0, + y: 5, + width: 40, + height: 75, + }); + + assert.equal(normalizeCanvasBounds({x: 120, y: 10, width: 20, height: 20}, {width: 100, height: 80}), null); +}); + +test('normalizeTextBounds keeps a useful minimum box inside the canvas', () => { + assert.deepEqual(normalizeTextBounds({x: 10, y: 20, width: 8, height: 8}, {width: 300, height: 200}), { + x: 10, + y: 20, + width: 160, + height: 48, + }); +}); + +test('fitNaturalSizeToCanvas scales large images and centers them', () => { + assert.deepEqual(fitNaturalSizeToCanvas({naturalWidth: 200, naturalHeight: 100}, {width: 100, height: 100}), { + x: 0, + y: 25, + width: 100, + height: 50, + }); +}); + +console.log('Editor geometry tests passed.'); diff --git a/app-v3/tests/layer-transforms.test.mjs b/app-v3/tests/layer-transforms.test.mjs index e1cbd21..2671e8c 100644 --- a/app-v3/tests/layer-transforms.test.mjs +++ b/app-v3/tests/layer-transforms.test.mjs @@ -1,7 +1,5 @@ import assert from 'node:assert/strict'; -import {readFile} from 'node:fs/promises'; -import {Buffer} from 'node:buffer'; -import ts from 'typescript'; +import {importTypescriptModule, test} from './test-support.mjs'; const transformModule = await importTypescriptModule('../src/core/layer-transforms.ts'); const {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} = transformModule; @@ -96,31 +94,6 @@ test('rotatePixelBuffer rotates square buffers right and left', () => { console.log('Layer transform tests passed.'); -async function importTypescriptModule(path) { - const url = new URL(path, import.meta.url); - const source = await readFile(url, 'utf8'); - const {outputText} = ts.transpileModule(source, { - compilerOptions: { - module: ts.ModuleKind.ES2022, - target: ts.ScriptTarget.ES2022, - strict: true, - }, - fileName: url.pathname, - }); - const encoded = Buffer.from(outputText).toString('base64'); - - return import(`data:text/javascript;base64,${encoded}`); -} - -function test(name, run) { - try { - run(); - } catch (error) { - error.message = `${name}: ${error.message}`; - throw error; - } -} - function pixelBuffer(width, height, pixels) { return { width, diff --git a/app-v3/tests/test-support.mjs b/app-v3/tests/test-support.mjs new file mode 100644 index 0000000..9f8eaca --- /dev/null +++ b/app-v3/tests/test-support.mjs @@ -0,0 +1,28 @@ +import {Buffer} from 'node:buffer'; +import {readFile} from 'node:fs/promises'; +import ts from 'typescript'; + +export async function importTypescriptModule(path) { + const url = new URL(path, import.meta.url); + const source = await readFile(url, 'utf8'); + const {outputText} = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ES2022, + target: ts.ScriptTarget.ES2022, + strict: true, + }, + fileName: url.pathname, + }); + const encoded = Buffer.from(outputText).toString('base64'); + + return import(`data:text/javascript;base64,${encoded}`); +} + +export function test(name, run) { + try { + run(); + } catch (error) { + error.message = `${name}: ${error.message}`; + throw error; + } +} From 59e522ebfb994a260ef9a930f202cb78d04b1499 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:33:16 +0200 Subject: [PATCH 18/62] Refactor: Modularize toolbar controls and sections, extract reusable utilities, and integrate streamlined toolbar structure with editor state syncing. --- app-v3/src/ui/toolbar-controls.ts | 131 +++++++++ app-v3/src/ui/toolbar-sections.ts | 291 +++++++++++++++++++ app-v3/src/ui/toolbar.ts | 459 +++--------------------------- 3 files changed, 463 insertions(+), 418 deletions(-) create mode 100644 app-v3/src/ui/toolbar-controls.ts create mode 100644 app-v3/src/ui/toolbar-sections.ts diff --git a/app-v3/src/ui/toolbar-controls.ts b/app-v3/src/ui/toolbar-controls.ts new file mode 100644 index 0000000..a612018 --- /dev/null +++ b/app-v3/src/ui/toolbar-controls.ts @@ -0,0 +1,131 @@ +export type CheckboxControl = { + element: HTMLLabelElement; + setChecked: (checked: boolean) => void; +}; + +export type RangeControl = { + element: HTMLLabelElement; + setValue: (value: number) => void; +}; + +export function createCommandButton(label: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'command-button'; + button.textContent = label; + button.addEventListener('click', onClick); + + return button; +} + +export function createNumberInput(label: string, value: number): HTMLInputElement { + const input = document.createElement('input'); + input.type = 'number'; + input.className = 'number-control'; + input.min = '64'; + input.max = '4096'; + input.step = '1'; + input.value = String(value); + input.placeholder = label; + input.title = label; + + return input; +} + +export function createCheckboxControl( + label: string, + checked: boolean, + onChange: (checked: boolean) => void, +): CheckboxControl { + const element = document.createElement('label'); + element.className = 'checkbox-control'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = checked; + input.addEventListener('change', () => onChange(input.checked)); + + const text = document.createElement('span'); + text.textContent = label; + element.append(input, text); + + return { + element, + setChecked: (nextChecked: boolean) => { + input.checked = nextChecked; + }, + }; +} + +export function createRangeControl(options: { + label: string; + min: number; + max: number; + value: number; + onChange: (value: number) => void; +}): RangeControl { + const element = document.createElement('label'); + element.className = 'range-control'; + element.textContent = options.label; + + const value = document.createElement('span'); + value.textContent = String(options.value); + + const input = document.createElement('input'); + input.type = 'range'; + input.min = String(options.min); + input.max = String(options.max); + input.value = String(options.value); + input.addEventListener('input', () => { + const nextValue = Number(input.value); + value.textContent = String(nextValue); + options.onChange(nextValue); + }); + + element.append(input, value); + + return { + element, + setValue: (nextValue: number) => { + input.value = String(nextValue); + value.textContent = String(nextValue); + }, + }; +} + +export function createSelectControl( + title: string, + options: readonly T[], + onChange: (value: T) => void, +): HTMLSelectElement { + const select = document.createElement('select'); + select.className = 'select-control'; + select.title = title; + + for (const option of options) { + const item = document.createElement('option'); + item.value = option; + item.textContent = option; + select.append(item); + } + + select.addEventListener('change', () => onChange(select.value as T)); + + return select; +} + +export function createGrid(className: string, ...children: HTMLElement[]): HTMLDivElement { + const grid = document.createElement('div'); + grid.className = className; + grid.append(...children); + + return grid; +} + +export function createToolbarSection(...children: HTMLElement[]): HTMLElement { + const section = document.createElement('section'); + section.className = 'toolbar__section'; + section.append(...children); + + return section; +} diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts new file mode 100644 index 0000000..1e93a28 --- /dev/null +++ b/app-v3/src/ui/toolbar-sections.ts @@ -0,0 +1,291 @@ +import {Tool} from '../core/types'; +import type {WebDraftEditor} from '../core/webdraft-editor'; +import {createColorPicker} from './color-picker'; +import { + createCheckboxControl, + createCommandButton, + createGrid, + createNumberInput, + createRangeControl, + createSelectControl, + createToolbarSection, +} from './toolbar-controls'; + +type ToolbarSection = { + element: HTMLElement; + sync: () => void; +}; + +type ToolConfig = { + id: Tool; + label: string; + icon: string; +}; + +const tools: ToolConfig[] = [ + {id: Tool.Select, label: 'Select', icon: 'S'}, + {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, + {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, + {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, + {id: Tool.Web, label: 'Web', icon: 'W'}, + {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, + {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, + {id: Tool.Text, label: 'Text', icon: 'T'}, +]; + +export function createToolSection(editor: WebDraftEditor): ToolbarSection { + const toolGroup = document.createElement('div'); + toolGroup.className = 'toolbar__group'; + + const toolButtons = new Map(); + + for (const tool of tools) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'tool-button'; + button.dataset.tool = tool.id; + button.title = tool.label; + button.textContent = tool.icon; + button.addEventListener('click', () => { + editor.setTool(tool.id); + }); + + toolButtons.set(tool.id, button); + toolGroup.append(button); + } + + return { + element: createToolbarSection(toolGroup), + sync: () => { + for (const [tool, button] of toolButtons) { + button.classList.toggle('is-active', tool === editor.state.activeTool); + } + }, + }; +} + +export function createStyleSection(editor: WebDraftEditor): ToolbarSection { + const sizeControl = createRangeControl({ + label: 'Size', + min: 1, + max: 120, + value: editor.state.size, + onChange: (value) => editor.setSize(value), + }); + + const webSensitivityControl = createRangeControl({ + label: 'Web', + min: 20, + max: 260, + value: editor.state.webSensitivity, + onChange: (value) => editor.setWebSensitivity(value), + }); + + const colorPicker = createColorPicker({ + label: 'Color', + value: editor.state.color, + onChange: (color) => editor.setColor(color), + }); + + const fillToggle = createCheckboxControl('Fill shapes', editor.state.fillEnabled, (enabled) => { + editor.setFillEnabled(enabled); + }); + + const fillColorPicker = createColorPicker({ + label: 'Fill color', + value: editor.state.fillColor, + onChange: (color) => editor.setFillColor(color), + }); + + const fillOpacityControl = createRangeControl({ + label: 'Fill opacity', + min: 0, + max: 100, + value: editor.state.fillOpacity, + onChange: (value) => editor.setFillOpacity(value), + }); + + return { + element: createToolbarSection( + colorPicker, + fillToggle.element, + fillColorPicker, + fillOpacityControl.element, + sizeControl.element, + webSensitivityControl.element, + ), + sync: () => { + colorPicker.setValue(editor.state.color); + fillToggle.setChecked(editor.state.fillEnabled); + fillColorPicker.setValue(editor.state.fillColor); + fillOpacityControl.setValue(editor.state.fillOpacity); + sizeControl.setValue(editor.state.size); + webSensitivityControl.setValue(editor.state.webSensitivity); + }, + }; +} + +export function createShadowSection(editor: WebDraftEditor): ToolbarSection { + const shadowToggle = createCheckboxControl('Shadow', editor.state.shadowEnabled, (enabled) => { + editor.setShadowEnabled(enabled); + }); + + const shadowColorPicker = createColorPicker({ + label: 'Shadow color', + value: editor.state.shadowColor, + onChange: (color) => editor.setShadowColor(color), + }); + + const shadowBlurControl = createRangeControl({ + label: 'Shadow blur', + min: 0, + max: 80, + value: editor.state.shadowBlur, + onChange: (value) => editor.setShadowBlur(value), + }); + + const shadowXControl = createRangeControl({ + label: 'Shadow X', + min: -120, + max: 120, + value: editor.state.shadowOffsetX, + onChange: (value) => editor.setShadowOffsetX(value), + }); + + const shadowYControl = createRangeControl({ + label: 'Shadow Y', + min: -120, + max: 120, + value: editor.state.shadowOffsetY, + onChange: (value) => editor.setShadowOffsetY(value), + }); + + return { + element: createToolbarSection( + shadowToggle.element, + shadowColorPicker, + shadowBlurControl.element, + shadowXControl.element, + shadowYControl.element, + ), + sync: () => { + shadowToggle.setChecked(editor.state.shadowEnabled); + shadowColorPicker.setValue(editor.state.shadowColor); + shadowBlurControl.setValue(editor.state.shadowBlur); + shadowXControl.setValue(editor.state.shadowOffsetX); + shadowYControl.setValue(editor.state.shadowOffsetY); + }, + }; +} + +export function createTextSection(editor: WebDraftEditor): ToolbarSection { + const fontSelect = createSelectControl('Text font', ['sans-serif', 'serif', 'monospace', 'cursive'], (font) => { + editor.setTextFontFamily(font); + }); + + const alignSelect = createSelectControl('Text alignment', ['left', 'center', 'right'] as const, (align) => { + editor.setTextAlign(align); + }); + + const textBoldToggle = createCheckboxControl('Bold', editor.state.textBold, (enabled) => { + editor.setTextBold(enabled); + }); + const textItalicToggle = createCheckboxControl('Italic', editor.state.textItalic, (enabled) => { + editor.setTextItalic(enabled); + }); + + return { + element: createToolbarSection( + createGrid('field-grid', fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element), + ), + sync: () => { + fontSelect.value = editor.state.textFontFamily; + alignSelect.value = editor.state.textAlign; + textBoldToggle.setChecked(editor.state.textBold); + textItalicToggle.setChecked(editor.state.textItalic); + }, + }; +} + +export function createEditSection(editor: WebDraftEditor): ToolbarSection { + const clearButton = createCommandButton('Clear', () => editor.clear()); + const undoButton = createCommandButton('Undo', () => editor.undo()); + const redoButton = createCommandButton('Redo', () => editor.redo()); + const copyButton = createCommandButton('Copy', () => editor.copySelection()); + const cutButton = createCommandButton('Cut', () => editor.cutSelection()); + const pasteButton = createCommandButton('Paste', () => editor.pasteSelection()); + + const invertButton = createCommandButton('Invert', () => editor.invertActiveLayer()); + const rotateLeftButton = createCommandButton('Rotate L', () => editor.rotateActiveLayer('left')); + const rotateRightButton = createCommandButton('Rotate R', () => editor.rotateActiveLayer('right')); + const mirrorHorizontalButton = createCommandButton('Mirror H', () => editor.mirrorActiveLayer('horizontal')); + const mirrorVerticalButton = createCommandButton('Mirror V', () => editor.mirrorActiveLayer('vertical')); + + invertButton.title = 'Invert colors'; + rotateLeftButton.title = 'Rotate left'; + rotateRightButton.title = 'Rotate right'; + mirrorHorizontalButton.title = 'Mirror horizontally'; + mirrorVerticalButton.title = 'Mirror vertically'; + + return { + element: createToolbarSection( + createGrid('command-grid', clearButton, undoButton, redoButton, copyButton, cutButton, pasteButton), + createGrid( + 'command-grid', + invertButton, + rotateLeftButton, + rotateRightButton, + mirrorHorizontalButton, + mirrorVerticalButton, + ), + ), + sync: () => { + undoButton.disabled = !editor.canUndo; + redoButton.disabled = !editor.canRedo; + copyButton.disabled = !editor.hasSelection; + cutButton.disabled = !editor.hasSelection; + pasteButton.disabled = !editor.canPaste; + }, + }; +} + +export function createResizeSection(editor: WebDraftEditor): ToolbarSection { + const resizeWidthInput = createNumberInput('Width', editor.state.canvasWidth); + const resizeHeightInput = createNumberInput('Height', editor.state.canvasHeight); + const resizeButton = createCommandButton('Resize canvas', () => { + editor.resizeCanvas(Number(resizeWidthInput.value), Number(resizeHeightInput.value)); + }); + + return { + element: createToolbarSection(createGrid('field-grid', resizeWidthInput, resizeHeightInput, resizeButton)), + sync: () => { + resizeWidthInput.value = String(editor.state.canvasWidth); + resizeHeightInput.value = String(editor.state.canvasHeight); + }, + }; +} + +export function createFileSection( + editor: WebDraftEditor, + fileInput: HTMLInputElement, + exportImage: () => Promise, +): ToolbarSection { + const uploadButton = createCommandButton('Upload image', () => fileInput.click()); + const cameraButton = createCommandButton('Camera snap', () => { + void editor.importCameraFrame(); + }); + const exportButton = createCommandButton('Export PNG', () => { + void exportImage(); + }); + + return { + element: createToolbarSection(createGrid('toolbar__stack', uploadButton, cameraButton, exportButton, fileInput)), + sync: () => {}, + }; +} + +export function syncToolbarSections(sections: ToolbarSection[]): void { + for (const section of sections) { + section.sync(); + } +} diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 78e8146..0ef083d 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,25 +1,17 @@ -import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; -import {createColorPicker} from './color-picker'; import {bindKeyboardShortcuts} from './keyboard-shortcuts'; import {createLayersPanel} from './layers-panel'; - -type ToolConfig = { - id: Tool; - label: string; - icon: string; -}; - -const tools: ToolConfig[] = [ - {id: Tool.Select, label: 'Select', icon: 'S'}, - {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, - {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, - {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, - {id: Tool.Web, label: 'Web', icon: 'W'}, - {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, - {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, - {id: Tool.Text, label: 'Text', icon: 'T'}, -]; +import { + createEditSection, + createFileSection, + createResizeSection, + createShadowSection, + createStyleSection, + createTextSection, + createToolSection, + syncToolbarSections, +} from './toolbar-sections'; +import {createToolbarSection} from './toolbar-controls'; export function createToolbar(editor: WebDraftEditor): HTMLElement { const toolbar = document.createElement('div'); @@ -28,247 +20,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { const title = document.createElement('h1'); title.textContent = 'WebDraft'; - const toolGroup = document.createElement('div'); - toolGroup.className = 'toolbar__group'; - - const toolButtons = new Map(); - - for (const tool of tools) { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'tool-button'; - button.dataset.tool = tool.id; - button.title = tool.label; - button.textContent = tool.icon; - - button.addEventListener('click', () => { - editor.setTool(tool.id); - }); - - toolButtons.set(tool.id, button); - toolGroup.append(button); - } - - const sizeControl = document.createElement('label'); - sizeControl.className = 'range-control'; - sizeControl.textContent = 'Size'; - - const sizeValue = document.createElement('span'); - sizeValue.textContent = String(editor.state.size); - - const sizeInput = document.createElement('input'); - sizeInput.type = 'range'; - sizeInput.min = '1'; - sizeInput.max = '120'; - sizeInput.value = String(editor.state.size); - sizeInput.addEventListener('input', () => { - const size = Number(sizeInput.value); - sizeValue.textContent = String(size); - editor.setSize(size); - }); - - sizeControl.append(sizeInput, sizeValue); - - const webSensitivityControl = document.createElement('label'); - webSensitivityControl.className = 'range-control'; - webSensitivityControl.textContent = 'Web'; - - const webSensitivityValue = document.createElement('span'); - webSensitivityValue.textContent = String(editor.state.webSensitivity); - - const webSensitivityInput = document.createElement('input'); - webSensitivityInput.type = 'range'; - webSensitivityInput.min = '20'; - webSensitivityInput.max = '260'; - webSensitivityInput.value = String(editor.state.webSensitivity); - webSensitivityInput.addEventListener('input', () => { - const sensitivity = Number(webSensitivityInput.value); - webSensitivityValue.textContent = String(sensitivity); - editor.setWebSensitivity(sensitivity); - }); - - webSensitivityControl.append(webSensitivityInput, webSensitivityValue); - - const colorPicker = createColorPicker({ - label: 'Color', - value: editor.state.color, - onChange: (color) => editor.setColor(color), - }); - - const fillToggle = document.createElement('label'); - fillToggle.className = 'checkbox-control'; - - const fillCheckbox = document.createElement('input'); - fillCheckbox.type = 'checkbox'; - fillCheckbox.checked = editor.state.fillEnabled; - fillCheckbox.addEventListener('change', () => { - editor.setFillEnabled(fillCheckbox.checked); - }); - - const fillText = document.createElement('span'); - fillText.textContent = 'Fill shapes'; - fillToggle.append(fillCheckbox, fillText); - - const fillColorPicker = createColorPicker({ - label: 'Fill color', - value: editor.state.fillColor, - onChange: (color) => editor.setFillColor(color), - }); - - const fillOpacityControl = createRangeControl({ - label: 'Fill opacity', - min: 0, - max: 100, - value: editor.state.fillOpacity, - onChange: (value) => editor.setFillOpacity(value), - }); - - const shadowToggle = createCheckboxControl('Shadow', editor.state.shadowEnabled, (enabled) => { - editor.setShadowEnabled(enabled); - }); - - const shadowColorPicker = createColorPicker({ - label: 'Shadow color', - value: editor.state.shadowColor, - onChange: (color) => editor.setShadowColor(color), - }); - - const shadowBlurControl = createRangeControl({ - label: 'Shadow blur', - min: 0, - max: 80, - value: editor.state.shadowBlur, - onChange: (value) => editor.setShadowBlur(value), - }); - - const shadowXControl = createRangeControl({ - label: 'Shadow X', - min: -120, - max: 120, - value: editor.state.shadowOffsetX, - onChange: (value) => editor.setShadowOffsetX(value), - }); - - const shadowYControl = createRangeControl({ - label: 'Shadow Y', - min: -120, - max: 120, - value: editor.state.shadowOffsetY, - onChange: (value) => editor.setShadowOffsetY(value), - }); - - const fontSelect = document.createElement('select'); - fontSelect.className = 'select-control'; - fontSelect.title = 'Text font'; - for (const option of ['sans-serif', 'serif', 'monospace', 'cursive']) { - const item = document.createElement('option'); - item.value = option; - item.textContent = option; - fontSelect.append(item); - } - fontSelect.addEventListener('change', () => editor.setTextFontFamily(fontSelect.value)); - - const alignSelect = document.createElement('select'); - alignSelect.className = 'select-control'; - alignSelect.title = 'Text alignment'; - for (const option of ['left', 'center', 'right'] as CanvasTextAlign[]) { - const item = document.createElement('option'); - item.value = option; - item.textContent = option; - alignSelect.append(item); - } - alignSelect.addEventListener('change', () => editor.setTextAlign(alignSelect.value as CanvasTextAlign)); - - const textBoldToggle = createCheckboxControl('Bold', editor.state.textBold, (enabled) => { - editor.setTextBold(enabled); - }); - const textItalicToggle = createCheckboxControl('Italic', editor.state.textItalic, (enabled) => { - editor.setTextItalic(enabled); - }); - - const resizeWidthInput = createNumberInput('Width', editor.state.canvasWidth); - const resizeHeightInput = createNumberInput('Height', editor.state.canvasHeight); - const resizeButton = createCommandButton('Resize canvas', () => { - editor.resizeCanvas(Number(resizeWidthInput.value), Number(resizeHeightInput.value)); - }); - - const clearButton = document.createElement('button'); - clearButton.type = 'button'; - clearButton.className = 'command-button'; - clearButton.textContent = 'Clear'; - clearButton.addEventListener('click', () => editor.clear()); - - const undoButton = document.createElement('button'); - undoButton.type = 'button'; - undoButton.className = 'command-button'; - undoButton.textContent = 'Undo'; - undoButton.addEventListener('click', () => editor.undo()); - - const redoButton = document.createElement('button'); - redoButton.type = 'button'; - redoButton.className = 'command-button'; - redoButton.textContent = 'Redo'; - redoButton.addEventListener('click', () => editor.redo()); - - const copyButton = document.createElement('button'); - copyButton.type = 'button'; - copyButton.className = 'command-button'; - copyButton.textContent = 'Copy'; - copyButton.addEventListener('click', () => editor.copySelection()); - - const cutButton = document.createElement('button'); - cutButton.type = 'button'; - cutButton.className = 'command-button'; - cutButton.textContent = 'Cut'; - cutButton.addEventListener('click', () => editor.cutSelection()); - - const pasteButton = document.createElement('button'); - pasteButton.type = 'button'; - pasteButton.className = 'command-button'; - pasteButton.textContent = 'Paste'; - pasteButton.addEventListener('click', () => editor.pasteSelection()); - - const transformGroup = document.createElement('div'); - transformGroup.className = 'command-grid'; - - const invertButton = createCommandButton('Invert', () => editor.invertActiveLayer()); - const rotateLeftButton = createCommandButton('Rotate L', () => editor.rotateActiveLayer('left')); - const rotateRightButton = createCommandButton('Rotate R', () => editor.rotateActiveLayer('right')); - const mirrorHorizontalButton = createCommandButton('Mirror H', () => editor.mirrorActiveLayer('horizontal')); - const mirrorVerticalButton = createCommandButton('Mirror V', () => editor.mirrorActiveLayer('vertical')); - - invertButton.title = 'Invert colors'; - rotateLeftButton.title = 'Rotate left'; - rotateRightButton.title = 'Rotate right'; - mirrorHorizontalButton.title = 'Mirror horizontally'; - mirrorVerticalButton.title = 'Mirror vertically'; - transformGroup.append(invertButton, rotateLeftButton, rotateRightButton, mirrorHorizontalButton, mirrorVerticalButton); - - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.className = 'visually-hidden'; - fileInput.addEventListener('change', async () => { - const file = fileInput.files?.[0]; - - if (!file) { - return; - } - - await editor.importImage(file); - fileInput.value = ''; - }); - - const uploadButton = document.createElement('button'); - uploadButton.type = 'button'; - uploadButton.className = 'command-button'; - uploadButton.textContent = 'Upload image'; - uploadButton.addEventListener('click', () => fileInput.click()); - - const exportButton = document.createElement('button'); - exportButton.type = 'button'; - exportButton.className = 'command-button'; - exportButton.textContent = 'Export PNG'; + const fileInput = createFileInput(editor); const exportImage = async () => { const blob = await editor.exportPng(); const url = URL.createObjectURL(blob); @@ -280,79 +32,24 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { URL.revokeObjectURL(url); }; - exportButton.addEventListener('click', () => { - void exportImage(); - }); - - const cameraButton = document.createElement('button'); - cameraButton.type = 'button'; - cameraButton.className = 'command-button'; - cameraButton.textContent = 'Camera snap'; - cameraButton.addEventListener('click', () => { - void editor.importCameraFrame(); - }); - - const renderState = () => { - for (const [tool, button] of toolButtons) { - button.classList.toggle('is-active', tool === editor.state.activeTool); - } - - sizeInput.value = String(editor.state.size); - sizeValue.textContent = String(editor.state.size); - webSensitivityInput.value = String(editor.state.webSensitivity); - webSensitivityValue.textContent = String(editor.state.webSensitivity); - colorPicker.setValue(editor.state.color); - fillColorPicker.setValue(editor.state.fillColor); - fillOpacityControl.setValue(editor.state.fillOpacity); - fillCheckbox.checked = editor.state.fillEnabled; - shadowToggle.setChecked(editor.state.shadowEnabled); - shadowColorPicker.setValue(editor.state.shadowColor); - shadowBlurControl.setValue(editor.state.shadowBlur); - shadowXControl.setValue(editor.state.shadowOffsetX); - shadowYControl.setValue(editor.state.shadowOffsetY); - fontSelect.value = editor.state.textFontFamily; - alignSelect.value = editor.state.textAlign; - textBoldToggle.setChecked(editor.state.textBold); - textItalicToggle.setChecked(editor.state.textItalic); - resizeWidthInput.value = String(editor.state.canvasWidth); - resizeHeightInput.value = String(editor.state.canvasHeight); - undoButton.disabled = !editor.canUndo; - redoButton.disabled = !editor.canRedo; - copyButton.disabled = !editor.hasSelection; - cutButton.disabled = !editor.hasSelection; - pasteButton.disabled = !editor.canPaste; - }; - - editor.addEventListener('change', renderState); - renderState(); - - const editGroup = document.createElement('div'); - editGroup.className = 'command-grid'; - editGroup.append(clearButton, undoButton, redoButton, copyButton, cutButton, pasteButton); - - const fileGroup = document.createElement('div'); - fileGroup.className = 'toolbar__stack'; - fileGroup.append(uploadButton, cameraButton, exportButton, fileInput); - - const resizeGroup = document.createElement('div'); - resizeGroup.className = 'field-grid'; - resizeGroup.append(resizeWidthInput, resizeHeightInput, resizeButton); + const sections = [ + createToolSection(editor), + createStyleSection(editor), + createShadowSection(editor), + createTextSection(editor), + createEditSection(editor), + createResizeSection(editor), + createFileSection(editor, fileInput, exportImage), + { + element: createToolbarSection(createLayersPanel(editor)), + sync: () => {}, + }, + ]; - const textGroup = document.createElement('div'); - textGroup.className = 'field-grid'; - textGroup.append(fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element); + editor.addEventListener('change', () => syncToolbarSections(sections)); + syncToolbarSections(sections); - toolbar.append( - title, - createToolbarSection(toolGroup), - createToolbarSection(colorPicker, fillToggle, fillColorPicker, fillOpacityControl.element, sizeControl, webSensitivityControl), - createToolbarSection(shadowToggle.element, shadowColorPicker, shadowBlurControl.element, shadowXControl.element, shadowYControl.element), - createToolbarSection(textGroup), - createToolbarSection(editGroup, transformGroup), - createToolbarSection(resizeGroup), - createToolbarSection(fileGroup), - createToolbarSection(createLayersPanel(editor)), - ); + toolbar.append(title, ...sections.map((section) => section.element)); bindKeyboardShortcuts(editor, { openImagePicker: () => fileInput.click(), @@ -362,95 +59,21 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { return toolbar; } -function createCommandButton(label: string, onClick: () => void): HTMLButtonElement { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'command-button'; - button.textContent = label; - button.addEventListener('click', onClick); - - return button; -} - -function createNumberInput(label: string, value: number): HTMLInputElement { - const input = document.createElement('input'); - input.type = 'number'; - input.className = 'number-control'; - input.min = '64'; - input.max = '4096'; - input.step = '1'; - input.value = String(value); - input.placeholder = label; - input.title = label; - - return input; -} - -function createCheckboxControl( - label: string, - checked: boolean, - onChange: (checked: boolean) => void, -): {element: HTMLLabelElement; setChecked: (checked: boolean) => void} { - const element = document.createElement('label'); - element.className = 'checkbox-control'; - - const input = document.createElement('input'); - input.type = 'checkbox'; - input.checked = checked; - input.addEventListener('change', () => onChange(input.checked)); - - const text = document.createElement('span'); - text.textContent = label; - element.append(input, text); - - return { - element, - setChecked: (nextChecked: boolean) => { - input.checked = nextChecked; - }, - }; -} - -function createRangeControl(options: { - label: string; - min: number; - max: number; - value: number; - onChange: (value: number) => void; -}): {element: HTMLLabelElement; setValue: (value: number) => void} { - const element = document.createElement('label'); - element.className = 'range-control'; - element.textContent = options.label; +function createFileInput(editor: WebDraftEditor): HTMLInputElement { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.className = 'visually-hidden'; + fileInput.addEventListener('change', async () => { + const file = fileInput.files?.[0]; - const value = document.createElement('span'); - value.textContent = String(options.value); + if (!file) { + return; + } - const input = document.createElement('input'); - input.type = 'range'; - input.min = String(options.min); - input.max = String(options.max); - input.value = String(options.value); - input.addEventListener('input', () => { - const nextValue = Number(input.value); - value.textContent = String(nextValue); - options.onChange(nextValue); + await editor.importImage(file); + fileInput.value = ''; }); - element.append(input, value); - - return { - element, - setValue: (nextValue: number) => { - input.value = String(nextValue); - value.textContent = String(nextValue); - }, - }; -} - -function createToolbarSection(...children: HTMLElement[]): HTMLElement { - const section = document.createElement('section'); - section.className = 'toolbar__section'; - section.append(...children); - - return section; + return fileInput; } From 7e62210c7069b963e9633d62eb90e1cd95e1e111 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:42:18 +0200 Subject: [PATCH 19/62] Refactor: Introduce document-level history snapshot management and replace layer-specific snapshots. Extend `LayerManager` to support full document capture and restore. --- app-v3/src/core/layer-manager.ts | 77 +++++++++++++++++---- app-v3/src/core/webdraft-editor.ts | 103 +++++++++++++++++++---------- 2 files changed, 130 insertions(+), 50 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 1a9d26f..2efa95d 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -16,11 +16,22 @@ export type LayerSummary = { visible: boolean; }; -export type LayerSnapshot = { +type LayerSnapshot = { layerId: string; imageData: ImageData; }; +export type LayerDocumentSnapshot = { + activeLayerId: string; + layerCount: number; + layers: LayerSnapshotWithMetadata[]; +}; + +type LayerSnapshotWithMetadata = LayerSnapshot & { + name: string; + visible: boolean; +}; + export class LayerManager { private readonly root: HTMLElement; private readonly layers: Layer[] = []; @@ -120,23 +131,58 @@ export class LayerManager { } } - captureActiveLayer(): LayerSnapshot { - const layer = this.activeLayer; - + captureDocument(): LayerDocumentSnapshot { return { - layerId: layer.id, - imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height), + activeLayerId: this.activeLayerId, + layerCount: this.layerCount, + layers: this.layers.map((layer) => ({ + layerId: layer.id, + name: layer.name, + visible: layer.visible, + imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height), + })), }; } - restoreLayer(snapshot: LayerSnapshot): void { - const layer = this.assertLayer(snapshot.layerId); + restoreDocument(snapshot: LayerDocumentSnapshot): void { + for (const layer of this.layers) { + layer.canvas.remove(); + } - layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); - layer.context.putImageData(snapshot.imageData, 0, 0); - this.activeLayerId = layer.id; + this.layers.length = 0; + this.layerCount = snapshot.layerCount; + + for (const layerSnapshot of snapshot.layers) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Canvas 2D context is unavailable.'); + } + + canvas.width = layerSnapshot.imageData.width; + canvas.height = layerSnapshot.imageData.height; + canvas.className = 'drawing-layer'; + canvas.dataset.layerId = layerSnapshot.layerId; + canvas.hidden = !layerSnapshot.visible; + context.putImageData(layerSnapshot.imageData, 0, 0); + + this.layers.push({ + id: layerSnapshot.layerId, + name: layerSnapshot.name, + canvas, + context, + visible: layerSnapshot.visible, + }); + this.root.append(canvas); + } + + this.activeLayerId = snapshot.layers.some((layer) => layer.layerId === snapshot.activeLayerId) + ? snapshot.activeLayerId + : this.layers[0]?.id ?? ''; } + drawImageOnNewLayer( image: CanvasImageSource, width: number, @@ -154,13 +200,16 @@ export class LayerManager { this.activeLayerId = id; } - renameLayer(id: string, name: string): void { + renameLayer(id: string, name: string): boolean { const layer = this.assertLayer(id); const nextName = name.trim(); - if (nextName) { - layer.name = nextName; + if (!nextName || nextName === layer.name) { + return false; } + + layer.name = nextName; + return true; } deleteActiveLayer(): Layer | null { diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 4265bfe..c90751a 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,4 +1,4 @@ -import type {LayerSnapshot, LayerSummary} from './layer-manager'; +import type {LayerDocumentSnapshot, LayerSummary} from './layer-manager'; import { drawLine, drawPoint, @@ -34,7 +34,7 @@ export class WebDraftEditor extends EventTarget { private skipNextTextPointerDown = false; private webPoints: Point[] = []; private clipboard: ClipboardSnapshot | null = null; - private pendingHistorySnapshot: LayerSnapshot | null = null; + private pendingHistorySnapshot: HistorySnapshot | null = null; private readonly undoStack: HistoryEntry[] = []; private readonly redoStack: HistoryEntry[] = []; @@ -204,20 +204,19 @@ export class WebDraftEditor extends EventTarget { return; } + const before = this.captureHistorySnapshot(); this.state.canvasWidth = nextWidth; this.state.canvasHeight = nextHeight; this.layerManager.resizeLayers(nextWidth, nextHeight); this.applyCanvasSize(); - this.undoStack.length = 0; - this.redoStack.length = 0; this.clipboard = null; - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } clear(): void { - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); this.layerManager.clearActiveLayer(); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); } undo(): void { @@ -227,7 +226,7 @@ export class WebDraftEditor extends EventTarget { return; } - this.layerManager.restoreLayer(entry.before); + this.restoreHistorySnapshot(entry.before); this.redoStack.push(entry); this.dispatchChange(); } @@ -239,17 +238,18 @@ export class WebDraftEditor extends EventTarget { return; } - this.layerManager.restoreLayer(entry.after); + this.restoreHistorySnapshot(entry.after); this.undoStack.push(entry); this.dispatchChange(); } async importImage(file: File): Promise { + const before = this.captureHistorySnapshot(); const image = await this.loadImage(file); const target = fitNaturalSizeToCanvas(image, this.canvasSize); this.layerManager.drawImageOnNewLayer(image, this.state.canvasWidth, this.state.canvasHeight, target); - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } async importCameraFrame(): Promise { @@ -277,9 +277,10 @@ export class WebDraftEditor extends EventTarget { video.addEventListener('loadedmetadata', () => resolve(), {once: true}); }); + const before = this.captureHistorySnapshot(); const target = fitNaturalSizeToCanvas({naturalWidth: video.videoWidth, naturalHeight: video.videoHeight}, this.canvasSize); this.layerManager.drawImageOnNewLayer(video, this.state.canvasWidth, this.state.canvasHeight, target); - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } finally { stream.getTracks().forEach((track) => track.stop()); } @@ -312,13 +313,16 @@ export class WebDraftEditor extends EventTarget { } addLayer(): void { + const before = this.captureHistorySnapshot(); this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight); - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } deleteActiveLayer(): void { - this.layerManager.deleteActiveLayer(); - this.dispatchChange(); + const before = this.captureHistorySnapshot(); + if (this.layerManager.deleteActiveLayer()) { + this.pushHistory(before, this.captureHistorySnapshot()); + } } selectLayer(id: string): void { @@ -327,24 +331,29 @@ export class WebDraftEditor extends EventTarget { } renameLayer(id: string, name: string): void { - this.layerManager.renameLayer(id, name); - this.dispatchChange(); + const before = this.captureHistorySnapshot(); + if (this.layerManager.renameLayer(id, name)) { + this.pushHistory(before, this.captureHistorySnapshot()); + } } toggleLayerVisibility(id: string): void { + const before = this.captureHistorySnapshot(); this.layerManager.toggleVisibility(id); - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } moveActiveLayerUp(): void { + const before = this.captureHistorySnapshot(); if (this.layerManager.moveActiveLayerUp()) { - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } } moveActiveLayerDown(): void { + const before = this.captureHistorySnapshot(); if (this.layerManager.moveActiveLayerDown()) { - this.dispatchChange(); + this.pushHistory(before, this.captureHistorySnapshot()); } } @@ -379,7 +388,7 @@ export class WebDraftEditor extends EventTarget { return; } - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {context} = this.layerManager.activeLayer; this.clipboard = { @@ -387,7 +396,7 @@ export class WebDraftEditor extends EventTarget { imageData: context.getImageData(bounds.x, bounds.y, bounds.width, bounds.height), }; context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); this.dispatchChange(); } @@ -396,7 +405,7 @@ export class WebDraftEditor extends EventTarget { return; } - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {context} = this.layerManager.activeLayer; const target = this.selectionBounds ?? this.clipboard.bounds; const x = Math.round(target.x); @@ -410,7 +419,7 @@ export class WebDraftEditor extends EventTarget { height: this.clipboard.bounds.height, }; this.renderSelectionFrame(); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); this.dispatchChange(); } @@ -418,39 +427,39 @@ export class WebDraftEditor extends EventTarget { this.commitTextInput(); this.clearSelection(); - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {canvas, context} = this.layerManager.activeLayer; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const transformed = invertPixelBuffer(imageData); context.putImageData(createImageData(transformed), 0, 0); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); } rotateActiveLayer(direction: 'left' | 'right'): void { this.commitTextInput(); this.clearSelection(); - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {canvas, context} = this.layerManager.activeLayer; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const transformed = rotatePixelBuffer(imageData, direction); context.putImageData(createImageData(transformed), 0, 0); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); } mirrorActiveLayer(axis: 'horizontal' | 'vertical'): void { this.commitTextInput(); this.clearSelection(); - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {canvas, context} = this.layerManager.activeLayer; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const transformed = mirrorPixelBuffer(imageData, axis); context.putImageData(createImageData(transformed), 0, 0); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); } private bindPointerEvents(): void { @@ -496,7 +505,7 @@ export class WebDraftEditor extends EventTarget { return; } - this.pendingHistorySnapshot = this.layerManager.captureActiveLayer(); + this.pendingHistorySnapshot = this.captureHistorySnapshot(); if (this.isShapeTool()) { this.shapeStartPoint = this.lastPoint; @@ -757,11 +766,11 @@ export class WebDraftEditor extends EventTarget { return; } - const before = this.layerManager.captureActiveLayer(); + const before = this.captureHistorySnapshot(); const {context} = this.layerManager.activeLayer; drawText(context, value, bounds, this.state); - this.pushHistory(before, this.layerManager.captureActiveLayer()); + this.pushHistory(before, this.captureHistorySnapshot()); this.textBounds = null; } @@ -841,11 +850,27 @@ export class WebDraftEditor extends EventTarget { return; } - this.pushHistory(this.pendingHistorySnapshot, this.layerManager.captureActiveLayer()); + this.pushHistory(this.pendingHistorySnapshot, this.captureHistorySnapshot()); this.pendingHistorySnapshot = null; } - private pushHistory(before: LayerSnapshot, after: LayerSnapshot): void { + private captureHistorySnapshot(): HistorySnapshot { + return { + canvasWidth: this.state.canvasWidth, + canvasHeight: this.state.canvasHeight, + document: this.layerManager.captureDocument(), + }; + } + + private restoreHistorySnapshot(snapshot: HistorySnapshot): void { + this.state.canvasWidth = snapshot.canvasWidth; + this.state.canvasHeight = snapshot.canvasHeight; + this.layerManager.restoreDocument(snapshot.document); + this.applyCanvasSize(); + this.clearSelection(); + } + + private pushHistory(before: HistorySnapshot, after: HistorySnapshot): void { this.undoStack.push({before, after}); this.redoStack.length = 0; this.dispatchChange(); @@ -857,8 +882,14 @@ export class WebDraftEditor extends EventTarget { } type HistoryEntry = { - before: LayerSnapshot; - after: LayerSnapshot; + before: HistorySnapshot; + after: HistorySnapshot; +}; + +type HistorySnapshot = { + canvasWidth: number; + canvasHeight: number; + document: LayerDocumentSnapshot; }; type ClipboardSnapshot = { From 9c730dd6fe337f5a1c23061ae4da4de1eda734dc Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:48:47 +0200 Subject: [PATCH 20/62] Refactor: Replace undo/redo stack logic with `HistoryManager` for centralized history tracking, add unit tests, and update build script to include history tests. --- app-v3/package.json | 2 +- app-v3/src/core/history-manager.ts | 44 +++++++++++++++++++++++++++ app-v3/src/core/webdraft-editor.ts | 30 +++++++----------- app-v3/tests/history-manager.test.mjs | 34 +++++++++++++++++++++ 4 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 app-v3/src/core/history-manager.ts create mode 100644 app-v3/tests/history-manager.test.mjs diff --git a/app-v3/package.json b/app-v3/package.json index c5e004a..85771ba 100644 --- a/app-v3/package.json +++ b/app-v3/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", - "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs", + "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs", "build": "tsc && vite build", "preview": "vite preview --host 127.0.0.1" }, diff --git a/app-v3/src/core/history-manager.ts b/app-v3/src/core/history-manager.ts new file mode 100644 index 0000000..d7fd502 --- /dev/null +++ b/app-v3/src/core/history-manager.ts @@ -0,0 +1,44 @@ +type HistoryEntry = { + before: TSnapshot; + after: TSnapshot; +}; + +export class HistoryManager { + private readonly undoStack: HistoryEntry[] = []; + private readonly redoStack: HistoryEntry[] = []; + + get canUndo(): boolean { + return this.undoStack.length > 0; + } + + get canRedo(): boolean { + return this.redoStack.length > 0; + } + + push(before: TSnapshot, after: TSnapshot): void { + this.undoStack.push({before, after}); + this.redoStack.length = 0; + } + + undo(): TSnapshot | null { + const entry = this.undoStack.pop(); + + if (!entry) { + return null; + } + + this.redoStack.push(entry); + return entry.before; + } + + redo(): TSnapshot | null { + const entry = this.redoStack.pop(); + + if (!entry) { + return null; + } + + this.undoStack.push(entry); + return entry.after; + } +} diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index c90751a..43ffe56 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -13,6 +13,7 @@ import { normalizeCanvasBounds, normalizeTextBounds, } from './editor-geometry'; +import {HistoryManager} from './history-manager'; import {LayerManager} from './layer-manager'; import {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} from './layer-transforms'; import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; @@ -35,8 +36,7 @@ export class WebDraftEditor extends EventTarget { private webPoints: Point[] = []; private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: HistorySnapshot | null = null; - private readonly undoStack: HistoryEntry[] = []; - private readonly redoStack: HistoryEntry[] = []; + private readonly history = new HistoryManager(); readonly state: EditorState; @@ -94,11 +94,11 @@ export class WebDraftEditor extends EventTarget { } get canUndo(): boolean { - return this.undoStack.length > 0; + return this.history.canUndo; } get canRedo(): boolean { - return this.redoStack.length > 0; + return this.history.canRedo; } get hasSelection(): boolean { @@ -220,26 +220,24 @@ export class WebDraftEditor extends EventTarget { } undo(): void { - const entry = this.undoStack.pop(); + const snapshot = this.history.undo(); - if (!entry) { + if (!snapshot) { return; } - this.restoreHistorySnapshot(entry.before); - this.redoStack.push(entry); + this.restoreHistorySnapshot(snapshot); this.dispatchChange(); } redo(): void { - const entry = this.redoStack.pop(); + const snapshot = this.history.redo(); - if (!entry) { + if (!snapshot) { return; } - this.restoreHistorySnapshot(entry.after); - this.undoStack.push(entry); + this.restoreHistorySnapshot(snapshot); this.dispatchChange(); } @@ -871,8 +869,7 @@ export class WebDraftEditor extends EventTarget { } private pushHistory(before: HistorySnapshot, after: HistorySnapshot): void { - this.undoStack.push({before, after}); - this.redoStack.length = 0; + this.history.push(before, after); this.dispatchChange(); } @@ -881,11 +878,6 @@ export class WebDraftEditor extends EventTarget { } } -type HistoryEntry = { - before: HistorySnapshot; - after: HistorySnapshot; -}; - type HistorySnapshot = { canvasWidth: number; canvasHeight: number; diff --git a/app-v3/tests/history-manager.test.mjs b/app-v3/tests/history-manager.test.mjs new file mode 100644 index 0000000..072619a --- /dev/null +++ b/app-v3/tests/history-manager.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import {importTypescriptModule, test} from './test-support.mjs'; + +const historyModule = await importTypescriptModule('../src/core/history-manager.ts'); +const {HistoryManager} = historyModule; + +test('HistoryManager returns previous and next snapshots', () => { + const history = new HistoryManager(); + + history.push('a', 'b'); + + assert.equal(history.canUndo, true); + assert.equal(history.canRedo, false); + assert.equal(history.undo(), 'a'); + assert.equal(history.canUndo, false); + assert.equal(history.canRedo, true); + assert.equal(history.redo(), 'b'); + assert.equal(history.canUndo, true); + assert.equal(history.canRedo, false); +}); + +test('HistoryManager clears redo stack after a new branch', () => { + const history = new HistoryManager(); + + history.push('a', 'b'); + assert.equal(history.undo(), 'a'); + history.push('a', 'c'); + + assert.equal(history.canRedo, false); + assert.equal(history.redo(), null); + assert.equal(history.undo(), 'a'); +}); + +console.log('History manager tests passed.'); From 1bbcbfcf1bd0ea600f58c036eb0dabdb907ddffd Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:51:51 +0200 Subject: [PATCH 21/62] Add configurable history limit to `HistoryManager`, enforce trimming, and add unit tests --- app-v3/src/core/history-manager.ts | 17 +++++++++++++++++ app-v3/src/core/webdraft-editor.ts | 2 +- app-v3/tests/history-manager.test.mjs | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app-v3/src/core/history-manager.ts b/app-v3/src/core/history-manager.ts index d7fd502..2b0f519 100644 --- a/app-v3/src/core/history-manager.ts +++ b/app-v3/src/core/history-manager.ts @@ -7,6 +7,8 @@ export class HistoryManager { private readonly undoStack: HistoryEntry[] = []; private readonly redoStack: HistoryEntry[] = []; + constructor(private readonly limit = 30) {} + get canUndo(): boolean { return this.undoStack.length > 0; } @@ -17,6 +19,7 @@ export class HistoryManager { push(before: TSnapshot, after: TSnapshot): void { this.undoStack.push({before, after}); + this.trimUndoStack(); this.redoStack.length = 0; } @@ -39,6 +42,20 @@ export class HistoryManager { } this.undoStack.push(entry); + this.trimUndoStack(); return entry.after; } + + private trimUndoStack(): void { + if (this.limit < 1) { + this.undoStack.length = 0; + return; + } + + const excessEntries = this.undoStack.length - this.limit; + + if (excessEntries > 0) { + this.undoStack.splice(0, excessEntries); + } + } } diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 43ffe56..c4a67bb 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -36,7 +36,7 @@ export class WebDraftEditor extends EventTarget { private webPoints: Point[] = []; private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: HistorySnapshot | null = null; - private readonly history = new HistoryManager(); + private readonly history = new HistoryManager(30); readonly state: EditorState; diff --git a/app-v3/tests/history-manager.test.mjs b/app-v3/tests/history-manager.test.mjs index 072619a..89e3f3c 100644 --- a/app-v3/tests/history-manager.test.mjs +++ b/app-v3/tests/history-manager.test.mjs @@ -31,4 +31,25 @@ test('HistoryManager clears redo stack after a new branch', () => { assert.equal(history.undo(), 'a'); }); +test('HistoryManager trims the oldest undo entries when the limit is exceeded', () => { + const history = new HistoryManager(2); + + history.push('a', 'b'); + history.push('b', 'c'); + history.push('c', 'd'); + + assert.equal(history.undo(), 'c'); + assert.equal(history.undo(), 'b'); + assert.equal(history.undo(), null); +}); + +test('HistoryManager can disable history with a zero limit', () => { + const history = new HistoryManager(0); + + history.push('a', 'b'); + + assert.equal(history.canUndo, false); + assert.equal(history.undo(), null); +}); + console.log('History manager tests passed.'); From 1d94bb96f904aa945b26d14f98f15fdb154ad98c Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 21:56:21 +0200 Subject: [PATCH 22/62] Add status toast system with `createStatusToasts`, integrate into toolbar actions, and enhance UI for feedback handling --- app-v3/src/main.ts | 7 +++-- app-v3/src/styles.css | 45 +++++++++++++++++++++++++++++++ app-v3/src/ui/status-toasts.ts | 35 ++++++++++++++++++++++++ app-v3/src/ui/toolbar-sections.ts | 17 ++++++++++-- app-v3/src/ui/toolbar.ts | 41 ++++++++++++++++++---------- 5 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 app-v3/src/ui/status-toasts.ts diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index 3497421..ecb4bec 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -1,4 +1,5 @@ import { WebDraftEditor } from './core/webdraft-editor'; +import { createStatusToasts } from './ui/status-toasts'; import { createToolbar } from './ui/toolbar'; import './styles.css'; @@ -20,9 +21,11 @@ workspace.className = 'workspace'; const surface = document.createElement('div'); surface.className = 'canvas-surface'; +const statusToasts = createStatusToasts(); + workspace.append(surface); shell.append(sidebar, workspace); -app.append(shell); +app.append(shell, statusToasts); const editor = new WebDraftEditor(surface, { width: 900, @@ -31,5 +34,5 @@ const editor = new WebDraftEditor(surface, { size: 10 }); -sidebar.append(createToolbar(editor)); +sidebar.append(createToolbar(editor, statusToasts)); editor.mount(); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 3810828..21e92b6 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -396,6 +396,40 @@ select { border-left: 1px solid #343941; } +.status-toasts { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 50; + display: grid; + justify-items: end; + width: min(360px, calc(100vw - 36px)); + pointer-events: none; +} + +.status-toast { + width: fit-content; + max-width: 100%; + padding: 12px 14px; + border: 1px solid #3d4654; + border-radius: 6px; + color: #f4f7fb; + background: #1b1e24; + box-shadow: 0 16px 42px rgb(0 0 0 / 35%); + font-size: 13px; + line-height: 1.35; +} + +.status-toast--success { + border-color: #2f8f5b; + background: #173425; +} + +.status-toast--error { + border-color: #b64d5a; + background: #3a1d24; +} + @media (max-width: 760px) { .app-shell { grid-template-columns: 1fr; @@ -445,6 +479,17 @@ select { .layers-panel { min-width: 0; } + + .status-toasts { + right: 10px; + bottom: 10px; + width: calc(100vw - 20px); + justify-items: stretch; + } + + .status-toast { + width: 100%; + } } @media (max-width: 480px) { diff --git a/app-v3/src/ui/status-toasts.ts b/app-v3/src/ui/status-toasts.ts new file mode 100644 index 0000000..057578a --- /dev/null +++ b/app-v3/src/ui/status-toasts.ts @@ -0,0 +1,35 @@ +export type StatusTone = 'info' | 'success' | 'error'; + +export type StatusReporter = { + show: (message: string, tone?: StatusTone) => void; +}; + +export function createStatusToasts(): HTMLElement & StatusReporter { + const host = Object.assign(document.createElement('div'), { + show: (_message: string, _tone: StatusTone = 'info') => {}, + }); + host.className = 'status-toasts'; + host.setAttribute('aria-live', 'polite'); + host.show = (message, tone = 'info') => { + const toast = document.createElement('div'); + toast.className = `status-toast status-toast--${tone}`; + toast.textContent = message; + + host.replaceChildren(toast); + window.setTimeout(() => { + if (toast.parentElement === host) { + toast.remove(); + } + }, tone === 'error' ? 6000 : 3200); + }; + + return host; +} + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message; + } + + return 'Something went wrong.'; +} diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 1e93a28..0142eb0 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -1,6 +1,8 @@ import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {createColorPicker} from './color-picker'; +import type {StatusReporter} from './status-toasts'; +import {getErrorMessage} from './status-toasts'; import { createCheckboxControl, createCommandButton, @@ -249,11 +251,12 @@ export function createEditSection(editor: WebDraftEditor): ToolbarSection { }; } -export function createResizeSection(editor: WebDraftEditor): ToolbarSection { +export function createResizeSection(editor: WebDraftEditor, status?: StatusReporter): ToolbarSection { const resizeWidthInput = createNumberInput('Width', editor.state.canvasWidth); const resizeHeightInput = createNumberInput('Height', editor.state.canvasHeight); const resizeButton = createCommandButton('Resize canvas', () => { editor.resizeCanvas(Number(resizeWidthInput.value), Number(resizeHeightInput.value)); + status?.show(`Canvas resized to ${editor.state.canvasWidth} x ${editor.state.canvasHeight}.`, 'success'); }); return { @@ -269,10 +272,11 @@ export function createFileSection( editor: WebDraftEditor, fileInput: HTMLInputElement, exportImage: () => Promise, + status: StatusReporter, ): ToolbarSection { const uploadButton = createCommandButton('Upload image', () => fileInput.click()); const cameraButton = createCommandButton('Camera snap', () => { - void editor.importCameraFrame(); + void importCameraFrame(editor, status); }); const exportButton = createCommandButton('Export PNG', () => { void exportImage(); @@ -284,6 +288,15 @@ export function createFileSection( }; } +async function importCameraFrame(editor: WebDraftEditor, status: StatusReporter): Promise { + try { + await editor.importCameraFrame(); + status.show('Camera frame added.', 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } +} + export function syncToolbarSections(sections: ToolbarSection[]): void { for (const section of sections) { section.sync(); diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 0ef083d..87d8e8f 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,6 +1,8 @@ import type {WebDraftEditor} from '../core/webdraft-editor'; import {bindKeyboardShortcuts} from './keyboard-shortcuts'; import {createLayersPanel} from './layers-panel'; +import type {StatusReporter} from './status-toasts'; +import {getErrorMessage} from './status-toasts'; import { createEditSection, createFileSection, @@ -13,23 +15,28 @@ import { } from './toolbar-sections'; import {createToolbarSection} from './toolbar-controls'; -export function createToolbar(editor: WebDraftEditor): HTMLElement { +export function createToolbar(editor: WebDraftEditor, status: StatusReporter): HTMLElement { const toolbar = document.createElement('div'); toolbar.className = 'toolbar'; const title = document.createElement('h1'); title.textContent = 'WebDraft'; - const fileInput = createFileInput(editor); + const fileInput = createFileInput(editor, status); const exportImage = async () => { - const blob = await editor.exportPng(); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + try { + const blob = await editor.exportPng(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); - link.href = url; - link.download = 'webdraft-image.png'; - link.click(); - URL.revokeObjectURL(url); + link.href = url; + link.download = 'webdraft-image.png'; + link.click(); + URL.revokeObjectURL(url); + status.show('PNG exported.', 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } }; const sections = [ @@ -38,8 +45,8 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { createShadowSection(editor), createTextSection(editor), createEditSection(editor), - createResizeSection(editor), - createFileSection(editor, fileInput, exportImage), + createResizeSection(editor, status), + createFileSection(editor, fileInput, exportImage, status), { element: createToolbarSection(createLayersPanel(editor)), sync: () => {}, @@ -59,7 +66,7 @@ export function createToolbar(editor: WebDraftEditor): HTMLElement { return toolbar; } -function createFileInput(editor: WebDraftEditor): HTMLInputElement { +function createFileInput(editor: WebDraftEditor, status: StatusReporter): HTMLInputElement { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; @@ -71,8 +78,14 @@ function createFileInput(editor: WebDraftEditor): HTMLInputElement { return; } - await editor.importImage(file); - fileInput.value = ''; + try { + await editor.importImage(file); + status.show('Image imported.', 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } finally { + fileInput.value = ''; + } }); return fileInput; From 54c9e8e0ff44e5b053468a7a8b92057b0823c8a9 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 22:01:17 +0200 Subject: [PATCH 23/62] Add `manual-qa-checklist.md`, update `rewrite-checklist.md` for QA steps, and add `npm run check` script to package.json --- app-v3/package.json | 1 + docs/manual-qa-checklist.md | 89 +++++++++++++++++++++++++++++++++++++ docs/rewrite-checklist.md | 4 ++ 3 files changed, 94 insertions(+) create mode 100644 docs/manual-qa-checklist.md diff --git a/app-v3/package.json b/app-v3/package.json index 85771ba..8f4086b 100644 --- a/app-v3/package.json +++ b/app-v3/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite --host 127.0.0.1", "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs", + "check": "npm test && npm run build", "build": "tsc && vite build", "preview": "vite preview --host 127.0.0.1" }, diff --git a/docs/manual-qa-checklist.md b/docs/manual-qa-checklist.md new file mode 100644 index 0000000..ae45d08 --- /dev/null +++ b/docs/manual-qa-checklist.md @@ -0,0 +1,89 @@ +# WebDraft Manual QA Checklist + +Use this before replacing the old entrypoint or after larger editor changes. + +## Automated Gate + +- [ ] Run `npm run check` in `app-v3/`. +- [ ] Confirm the app opens at `http://127.0.0.1:5173/`. +- [ ] Confirm the browser console has no unexpected errors during basic use. + +## Drawing Tools + +- [ ] Pencil draws a continuous stroke and enables Undo. +- [ ] Eraser removes pixels from the active layer. +- [ ] Size slider changes brush width. +- [ ] Color picker changes stroke color. +- [ ] Sampler picks a visible non-transparent color and returns to Pencil. +- [ ] Web tool draws the main stroke plus nearby connecting lines. + +## Shapes And Text + +- [ ] Rectangle preview appears while dragging and commits on pointer up. +- [ ] Ellipse preview appears while dragging and commits on pointer up. +- [ ] Fill toggle, fill color, and fill opacity affect shapes. +- [ ] Text tool opens a textarea over the selected bounds. +- [ ] Text commits on blur. +- [ ] Text font, alignment, bold, italic, and size are reflected on commit. + +## Selection And Clipboard + +- [ ] Select tool draws a clipped selection frame. +- [ ] Copy enables Paste. +- [ ] Cut removes the selected pixels and enables Paste. +- [ ] Paste places pixels at the selection bounds. +- [ ] Delete or Backspace clears the active layer. + +## Layers + +- [ ] Add layer creates a new active layer. +- [ ] Delete layer removes the active layer but never removes the last layer. +- [ ] Move up/down changes layer order and visual stacking. +- [ ] Hide/show toggles visibility and export respects hidden layers. +- [ ] Rename keeps a non-empty name and ignores empty names. +- [ ] Layer thumbnails update after drawing and transforms. + +## History + +- [ ] Undo/redo works for drawing strokes. +- [ ] Undo/redo works for shapes and text. +- [ ] Undo/redo works for add/delete/move/rename/hide/show layer. +- [ ] Undo/redo works after image import. +- [ ] Undo/redo works after canvas resize. +- [ ] New edits after Undo clear the Redo stack. +- [ ] History remains bounded to the latest 30 operations. + +## Canvas And Files + +- [ ] Resize canvas changes the visible canvas and keeps existing layer pixels at the top-left. +- [ ] Upload image creates a new fitted layer. +- [ ] Export PNG downloads a composited image. +- [ ] Exported PNG excludes hidden layers. +- [ ] Camera snap either adds a new layer or shows a permission/device error toast. + +## Status And Errors + +- [ ] Successful image import shows a success toast. +- [ ] Successful PNG export shows a success toast. +- [ ] Successful resize shows the new canvas size in a toast. +- [ ] Camera denial or unsupported camera shows an error toast. +- [ ] Toasts do not block drawing or toolbar interaction. + +## Responsive Layout + +- [ ] Desktop sidebar scrolls independently from the canvas workspace. +- [ ] Desktop page has no accidental horizontal body overflow. +- [ ] Mobile toolbar scrolls horizontally. +- [ ] Mobile workspace scrolls the canvas instead of widening the page. +- [ ] Text inside buttons and controls remains readable at mobile width. + +## Keyboard Shortcuts + +- [ ] `S`, `P`, `E`, `C`, `W`, `R`, `O`, `T` switch tools. +- [ ] `[` and `]` change brush size. +- [ ] Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z undo/redo. +- [ ] Cmd/Ctrl+Y redoes. +- [ ] Cmd/Ctrl+O opens image picker. +- [ ] Cmd/Ctrl+S exports PNG. +- [ ] Cmd/Ctrl+C/X/V copy, cut, paste a selection. +- [ ] `I`, `,`, `.`, `H`, `V` run invert, rotate, and mirror actions. diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index fb24e90..62041be 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -37,3 +37,7 @@ - `js/modules/Shapes.js` - rectangle and circle behavior. - `js/modules/Text.js` - text selection and rendering behavior. - `parts/*.part.html` - current UI inventory. + +## Release Readiness + +- Use `docs/manual-qa-checklist.md` before replacing the old entrypoint or after larger editor changes. From b19aafeb3f9c8fbdc59736c08127bdb2573aa9aa Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 22:07:32 +0200 Subject: [PATCH 24/62] Add flood-fill functionality, implement fill bucket tool, and include corresponding tests and documentation updates --- app-v3/package.json | 2 +- app-v3/src/core/flood-fill.ts | 91 +++++++++++++++++++++++++++++ app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 27 +++++++++ app-v3/src/ui/keyboard-shortcuts.ts | 2 + app-v3/src/ui/toolbar-sections.ts | 1 + app-v3/tests/flood-fill.test.mjs | 78 +++++++++++++++++++++++++ docs/manual-qa-checklist.md | 3 +- docs/rewrite-checklist.md | 1 + 9 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 app-v3/src/core/flood-fill.ts create mode 100644 app-v3/tests/flood-fill.test.mjs diff --git a/app-v3/package.json b/app-v3/package.json index 8f4086b..e5381fb 100644 --- a/app-v3/package.json +++ b/app-v3/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", - "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs", + "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs && node tests/flood-fill.test.mjs", "check": "npm test && npm run build", "build": "tsc && vite build", "preview": "vite preview --host 127.0.0.1" diff --git a/app-v3/src/core/flood-fill.ts b/app-v3/src/core/flood-fill.ts new file mode 100644 index 0000000..3e5442f --- /dev/null +++ b/app-v3/src/core/flood-fill.ts @@ -0,0 +1,91 @@ +export type RgbaColor = { + red: number; + green: number; + blue: number; + alpha: number; +}; + +export function floodFillImageData(imageData: ImageData, x: number, y: number, color: RgbaColor): boolean { + const startX = Math.floor(x); + const startY = Math.floor(y); + + if (startX < 0 || startY < 0 || startX >= imageData.width || startY >= imageData.height) { + return false; + } + + const target = getPixel(imageData, startX, startY); + + if (colorsMatch(target, color)) { + return false; + } + + const stack: Array<[number, number]> = [[startX, startY]]; + + while (stack.length > 0) { + const [currentX, currentY] = stack.pop() as [number, number]; + + if (currentX < 0 || currentY < 0 || currentX >= imageData.width || currentY >= imageData.height) { + continue; + } + + if (!colorsMatch(getPixel(imageData, currentX, currentY), target)) { + continue; + } + + setPixel(imageData, currentX, currentY, color); + stack.push( + [currentX + 1, currentY], + [currentX - 1, currentY], + [currentX, currentY + 1], + [currentX, currentY - 1], + ); + } + + return true; +} + +export function hexToRgbaColor(hex: string, opacity: number): RgbaColor { + const normalized = hex.replace('#', ''); + + return { + red: Number.parseInt(normalized.slice(0, 2), 16), + green: Number.parseInt(normalized.slice(2, 4), 16), + blue: Number.parseInt(normalized.slice(4, 6), 16), + alpha: Math.round(Math.min(Math.max(opacity, 0), 1) * 255), + }; +} + +function getPixel(imageData: ImageData, x: number, y: number): RgbaColor { + const index = getPixelIndex(imageData.width, x, y); + const {data} = imageData; + + return { + red: data[index], + green: data[index + 1], + blue: data[index + 2], + alpha: data[index + 3], + }; +} + +function setPixel(imageData: ImageData, x: number, y: number, color: RgbaColor): void { + const index = getPixelIndex(imageData.width, x, y); + const {data} = imageData; + + data[index] = color.red; + data[index + 1] = color.green; + data[index + 2] = color.blue; + data[index + 3] = color.alpha; +} + +function colorsMatch(first: RgbaColor, second: RgbaColor): boolean { + return ( + first.red === second.red && + first.green === second.green && + first.blue === second.blue && + first.alpha === second.alpha + ); +} + +function getPixelIndex(width: number, x: number, y: number): number { + return (y * width + x) * 4; +} diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 9b569cb..19c5e30 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -3,6 +3,7 @@ export enum Tool { Pencil = 'pencil', Eraser = 'eraser', Sampler = 'sampler', + FillBucket = 'fill-bucket', Web = 'web', Rectangle = 'rectangle', Ellipse = 'ellipse', diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index c4a67bb..b9619e8 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -13,6 +13,7 @@ import { normalizeCanvasBounds, normalizeTextBounds, } from './editor-geometry'; +import {floodFillImageData, hexToRgbaColor} from './flood-fill'; import {HistoryManager} from './history-manager'; import {LayerManager} from './layer-manager'; import {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} from './layer-transforms'; @@ -503,6 +504,13 @@ export class WebDraftEditor extends EventTarget { return; } + if (this.state.activeTool === Tool.FillBucket) { + this.fillActiveLayer(this.lastPoint); + this.isDrawing = false; + this.lastPoint = null; + return; + } + this.pendingHistorySnapshot = this.captureHistorySnapshot(); if (this.isShapeTool()) { @@ -662,6 +670,25 @@ export class WebDraftEditor extends EventTarget { this.setColor(rgbToHex(red, green, blue)); } + private fillActiveLayer(point: Point): void { + const before = this.captureHistorySnapshot(); + const {canvas, context} = this.layerManager.activeLayer; + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const changed = floodFillImageData( + imageData, + point.x, + point.y, + hexToRgbaColor(this.state.fillColor, this.state.fillOpacity / 100), + ); + + if (!changed) { + return; + } + + context.putImageData(imageData, 0, 0); + this.pushHistory(before, this.captureHistorySnapshot()); + } + private renderSelectionPreview(point: Point): void { if (!this.selectionStartPoint) { return; diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index f20f7f2..67f9c9c 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -128,6 +128,8 @@ function getToolForKey(key: string): Tool | null { return Tool.Eraser; case 'c': return Tool.Sampler; + case 'b': + return Tool.FillBucket; case 'w': return Tool.Web; case 'r': diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 0142eb0..9da1b72 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -29,6 +29,7 @@ const tools: ToolConfig[] = [ {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, + {id: Tool.FillBucket, label: 'Fill bucket', icon: 'B'}, {id: Tool.Web, label: 'Web', icon: 'W'}, {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, diff --git a/app-v3/tests/flood-fill.test.mjs b/app-v3/tests/flood-fill.test.mjs new file mode 100644 index 0000000..ae20605 --- /dev/null +++ b/app-v3/tests/flood-fill.test.mjs @@ -0,0 +1,78 @@ +import assert from 'node:assert/strict'; +import {importTypescriptModule, test} from './test-support.mjs'; + +const floodFillModule = await importTypescriptModule('../src/core/flood-fill.ts'); +const {floodFillImageData, hexToRgbaColor} = floodFillModule; + +test('floodFillImageData fills only a contiguous matching region', () => { + const imageData = image(3, 3, [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [10, 10, 10, 255], + [0, 0, 0, 0], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + ]); + + const changed = floodFillImageData(imageData, 0, 0, {red: 200, green: 20, blue: 30, alpha: 255}); + + assert.equal(changed, true); + assert.deepEqual(pixels(imageData), [ + [200, 20, 30, 255], + [200, 20, 30, 255], + [10, 10, 10, 255], + [200, 20, 30, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + ]); +}); + +test('floodFillImageData returns false when replacement matches target', () => { + const imageData = image(1, 1, [[4, 5, 6, 255]]); + + assert.equal(floodFillImageData(imageData, 0, 0, {red: 4, green: 5, blue: 6, alpha: 255}), false); + assert.deepEqual(pixels(imageData), [[4, 5, 6, 255]]); +}); + +test('floodFillImageData ignores points outside image bounds', () => { + const imageData = image(1, 1, [[4, 5, 6, 255]]); + + assert.equal(floodFillImageData(imageData, 2, 0, {red: 9, green: 9, blue: 9, alpha: 255}), false); + assert.deepEqual(pixels(imageData), [[4, 5, 6, 255]]); +}); + +test('hexToRgbaColor clamps opacity into alpha channel', () => { + assert.deepEqual(hexToRgbaColor('#336699', 0.5), {red: 51, green: 102, blue: 153, alpha: 128}); + assert.deepEqual(hexToRgbaColor('#336699', 2), {red: 51, green: 102, blue: 153, alpha: 255}); +}); + +console.log('Flood fill tests passed.'); + +function image(width, height, sourcePixels) { + return { + width, + height, + data: new Uint8ClampedArray(sourcePixels.flat()), + }; +} + +function pixels(imageData) { + const result = []; + + for (let index = 0; index < imageData.data.length; index += 4) { + result.push([ + imageData.data[index], + imageData.data[index + 1], + imageData.data[index + 2], + imageData.data[index + 3], + ]); + } + + return result; +} diff --git a/docs/manual-qa-checklist.md b/docs/manual-qa-checklist.md index ae45d08..e47bab9 100644 --- a/docs/manual-qa-checklist.md +++ b/docs/manual-qa-checklist.md @@ -15,6 +15,7 @@ Use this before replacing the old entrypoint or after larger editor changes. - [ ] Size slider changes brush width. - [ ] Color picker changes stroke color. - [ ] Sampler picks a visible non-transparent color and returns to Pencil. +- [ ] Fill bucket fills a contiguous active-layer region using Fill color and Fill opacity. - [ ] Web tool draws the main stroke plus nearby connecting lines. ## Shapes And Text @@ -79,7 +80,7 @@ Use this before replacing the old entrypoint or after larger editor changes. ## Keyboard Shortcuts -- [ ] `S`, `P`, `E`, `C`, `W`, `R`, `O`, `T` switch tools. +- [ ] `S`, `P`, `E`, `C`, `B`, `W`, `R`, `O`, `T` switch tools. - [ ] `[` and `]` change brush size. - [ ] Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z undo/redo. - [ ] Cmd/Ctrl+Y redoes. diff --git a/docs/rewrite-checklist.md b/docs/rewrite-checklist.md index 62041be..d2c96db 100644 --- a/docs/rewrite-checklist.md +++ b/docs/rewrite-checklist.md @@ -22,6 +22,7 @@ - [x] Add fill options for shape tools. - [x] Add text tool. - [x] Add web drawing tool. +- [x] Add fill bucket tool. - [x] Add active-layer invert, mirror, and rotate actions. - [x] Add canvas resize, fill opacity, shadow controls, text options, and camera snap. - [x] Add undo/redo history. From 0c7eee061da7918fbb0db51cec257023d6c06fba Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 22:10:02 +0200 Subject: [PATCH 25/62] Add fill tolerance to flood-fill tool with UI control, update tests, and extend manual QA checklist --- app-v3/src/core/flood-fill.ts | 24 +++++++++++++++-------- app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 7 +++++++ app-v3/src/ui/toolbar-sections.ts | 10 ++++++++++ app-v3/tests/flood-fill.test.mjs | 31 ++++++++++++++++++++++++++++++ docs/manual-qa-checklist.md | 2 +- 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/app-v3/src/core/flood-fill.ts b/app-v3/src/core/flood-fill.ts index 3e5442f..c5bb4c2 100644 --- a/app-v3/src/core/flood-fill.ts +++ b/app-v3/src/core/flood-fill.ts @@ -5,7 +5,13 @@ export type RgbaColor = { alpha: number; }; -export function floodFillImageData(imageData: ImageData, x: number, y: number, color: RgbaColor): boolean { +export function floodFillImageData( + imageData: ImageData, + x: number, + y: number, + color: RgbaColor, + tolerance = 0, +): boolean { const startX = Math.floor(x); const startY = Math.floor(y); @@ -15,7 +21,7 @@ export function floodFillImageData(imageData: ImageData, x: number, y: number, c const target = getPixel(imageData, startX, startY); - if (colorsMatch(target, color)) { + if (colorsMatch(target, color, 0)) { return false; } @@ -28,7 +34,7 @@ export function floodFillImageData(imageData: ImageData, x: number, y: number, c continue; } - if (!colorsMatch(getPixel(imageData, currentX, currentY), target)) { + if (!colorsMatch(getPixel(imageData, currentX, currentY), target, tolerance)) { continue; } @@ -77,12 +83,14 @@ function setPixel(imageData: ImageData, x: number, y: number, color: RgbaColor): data[index + 3] = color.alpha; } -function colorsMatch(first: RgbaColor, second: RgbaColor): boolean { +function colorsMatch(first: RgbaColor, second: RgbaColor, tolerance: number): boolean { + const threshold = Math.min(Math.max(tolerance, 0), 255); + return ( - first.red === second.red && - first.green === second.green && - first.blue === second.blue && - first.alpha === second.alpha + Math.abs(first.red - second.red) <= threshold && + Math.abs(first.green - second.green) <= threshold && + Math.abs(first.blue - second.blue) <= threshold && + Math.abs(first.alpha - second.alpha) <= threshold ); } diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 19c5e30..70eb888 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -35,6 +35,7 @@ export type EditorState = { fillColor: string; fillEnabled: boolean; fillOpacity: number; + fillTolerance: number; size: number; canvasWidth: number; canvasHeight: number; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index b9619e8..b5e1aa8 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -65,6 +65,7 @@ export class WebDraftEditor extends EventTarget { fillColor: '#ffffff', fillEnabled: false, fillOpacity: 100, + fillTolerance: 0, size: options.size, canvasWidth: options.width, canvasHeight: options.height, @@ -139,6 +140,11 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setFillTolerance(tolerance: number): void { + this.state.fillTolerance = Math.min(Math.max(tolerance, 0), 255); + this.dispatchChange(); + } + setSize(size: number): void { this.state.size = Math.min(Math.max(size, 1), 120); this.dispatchChange(); @@ -679,6 +685,7 @@ export class WebDraftEditor extends EventTarget { point.x, point.y, hexToRgbaColor(this.state.fillColor, this.state.fillOpacity / 100), + this.state.fillTolerance, ); if (!changed) { diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 9da1b72..b4aa6a0 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -108,12 +108,21 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { onChange: (value) => editor.setFillOpacity(value), }); + const fillToleranceControl = createRangeControl({ + label: 'Bucket tolerance', + min: 0, + max: 255, + value: editor.state.fillTolerance, + onChange: (value) => editor.setFillTolerance(value), + }); + return { element: createToolbarSection( colorPicker, fillToggle.element, fillColorPicker, fillOpacityControl.element, + fillToleranceControl.element, sizeControl.element, webSensitivityControl.element, ), @@ -122,6 +131,7 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { fillToggle.setChecked(editor.state.fillEnabled); fillColorPicker.setValue(editor.state.fillColor); fillOpacityControl.setValue(editor.state.fillOpacity); + fillToleranceControl.setValue(editor.state.fillTolerance); sizeControl.setValue(editor.state.size); webSensitivityControl.setValue(editor.state.webSensitivity); }, diff --git a/app-v3/tests/flood-fill.test.mjs b/app-v3/tests/flood-fill.test.mjs index ae20605..fd7fa56 100644 --- a/app-v3/tests/flood-fill.test.mjs +++ b/app-v3/tests/flood-fill.test.mjs @@ -47,6 +47,37 @@ test('floodFillImageData ignores points outside image bounds', () => { assert.deepEqual(pixels(imageData), [[4, 5, 6, 255]]); }); +test('floodFillImageData fills similar colors when tolerance allows it', () => { + const imageData = image(3, 1, [ + [10, 10, 10, 255], + [12, 9, 10, 255], + [30, 30, 30, 255], + ]); + + const changed = floodFillImageData(imageData, 0, 0, {red: 200, green: 0, blue: 0, alpha: 255}, 3); + + assert.equal(changed, true); + assert.deepEqual(pixels(imageData), [ + [200, 0, 0, 255], + [200, 0, 0, 255], + [30, 30, 30, 255], + ]); +}); + +test('floodFillImageData keeps similar colors outside a zero tolerance fill', () => { + const imageData = image(2, 1, [ + [10, 10, 10, 255], + [11, 10, 10, 255], + ]); + + floodFillImageData(imageData, 0, 0, {red: 200, green: 0, blue: 0, alpha: 255}); + + assert.deepEqual(pixels(imageData), [ + [200, 0, 0, 255], + [11, 10, 10, 255], + ]); +}); + test('hexToRgbaColor clamps opacity into alpha channel', () => { assert.deepEqual(hexToRgbaColor('#336699', 0.5), {red: 51, green: 102, blue: 153, alpha: 128}); assert.deepEqual(hexToRgbaColor('#336699', 2), {red: 51, green: 102, blue: 153, alpha: 255}); diff --git a/docs/manual-qa-checklist.md b/docs/manual-qa-checklist.md index e47bab9..e779887 100644 --- a/docs/manual-qa-checklist.md +++ b/docs/manual-qa-checklist.md @@ -15,7 +15,7 @@ Use this before replacing the old entrypoint or after larger editor changes. - [ ] Size slider changes brush width. - [ ] Color picker changes stroke color. - [ ] Sampler picks a visible non-transparent color and returns to Pencil. -- [ ] Fill bucket fills a contiguous active-layer region using Fill color and Fill opacity. +- [ ] Fill bucket fills a contiguous active-layer region using Fill color, Fill opacity, and Bucket tolerance. - [ ] Web tool draws the main stroke plus nearby connecting lines. ## Shapes And Text From ccf21ae1f96ceb2b625ed3d266d67339da01dc3e Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 22:11:43 +0200 Subject: [PATCH 26/62] Add visited pixel tracking to flood-fill algorithm and include test for termination with tolerance --- app-v3/src/core/flood-fill.ts | 9 +++++++++ app-v3/tests/flood-fill.test.mjs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/app-v3/src/core/flood-fill.ts b/app-v3/src/core/flood-fill.ts index c5bb4c2..327fade 100644 --- a/app-v3/src/core/flood-fill.ts +++ b/app-v3/src/core/flood-fill.ts @@ -26,6 +26,7 @@ export function floodFillImageData( } const stack: Array<[number, number]> = [[startX, startY]]; + const visited = new Uint8Array(imageData.width * imageData.height); while (stack.length > 0) { const [currentX, currentY] = stack.pop() as [number, number]; @@ -34,6 +35,14 @@ export function floodFillImageData( continue; } + const visitIndex = currentY * imageData.width + currentX; + + if (visited[visitIndex]) { + continue; + } + + visited[visitIndex] = 1; + if (!colorsMatch(getPixel(imageData, currentX, currentY), target, tolerance)) { continue; } diff --git a/app-v3/tests/flood-fill.test.mjs b/app-v3/tests/flood-fill.test.mjs index fd7fa56..57c8ba8 100644 --- a/app-v3/tests/flood-fill.test.mjs +++ b/app-v3/tests/flood-fill.test.mjs @@ -78,6 +78,25 @@ test('floodFillImageData keeps similar colors outside a zero tolerance fill', () ]); }); +test('floodFillImageData terminates when replacement color is inside tolerance', () => { + const imageData = image(2, 2, [ + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + [10, 10, 10, 255], + ]); + + const changed = floodFillImageData(imageData, 0, 0, {red: 12, green: 10, blue: 10, alpha: 255}, 3); + + assert.equal(changed, true); + assert.deepEqual(pixels(imageData), [ + [12, 10, 10, 255], + [12, 10, 10, 255], + [12, 10, 10, 255], + [12, 10, 10, 255], + ]); +}); + test('hexToRgbaColor clamps opacity into alpha channel', () => { assert.deepEqual(hexToRgbaColor('#336699', 0.5), {red: 51, green: 102, blue: 153, alpha: 128}); assert.deepEqual(hexToRgbaColor('#336699', 2), {red: 51, green: 102, blue: 153, alpha: 255}); From 03cb0bec6e248fb982e94950c1858d091213604c Mon Sep 17 00:00:00 2001 From: rtcoder Date: Mon, 8 Jun 2026 22:16:07 +0200 Subject: [PATCH 27/62] Add `getClippedPasteBounds` to constrain pasting within canvas boundaries, update paste logic, and add tests --- app-v3/src/core/editor-geometry.ts | 35 +++++++++++++++++++++++++++ app-v3/src/core/webdraft-editor.ts | 22 +++++++++++++---- app-v3/tests/editor-geometry.test.mjs | 25 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/app-v3/src/core/editor-geometry.ts b/app-v3/src/core/editor-geometry.ts index 440dae1..cb7bf7c 100644 --- a/app-v3/src/core/editor-geometry.ts +++ b/app-v3/src/core/editor-geometry.ts @@ -5,6 +5,15 @@ export type NaturalSize = { naturalHeight: number; }; +export type ClippedPasteBounds = { + targetX: number; + targetY: number; + sourceX: number; + sourceY: number; + width: number; + height: number; +}; + export function getBounds(start: Point, end: Point): SizeWithPosition { const x = Math.min(start.x, end.x); const y = Math.min(start.y, end.y); @@ -50,3 +59,29 @@ export function normalizeTextBounds(bounds: SizeWithPosition, canvasSize: Size): return {x, y, width, height}; } + +export function getClippedPasteBounds(target: Point, imageSize: Size, canvasSize: Size): ClippedPasteBounds | null { + const x = Math.round(target.x); + const y = Math.round(target.y); + const sourceX = Math.max(0, -x); + const sourceY = Math.max(0, -y); + const targetX = Math.max(0, x); + const targetY = Math.max(0, y); + const right = Math.min(canvasSize.width, x + imageSize.width); + const bottom = Math.min(canvasSize.height, y + imageSize.height); + const width = right - targetX; + const height = bottom - targetY; + + if (width < 1 || height < 1) { + return null; + } + + return { + targetX, + targetY, + sourceX, + sourceY, + width, + height, + }; +} diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index b5e1aa8..3a3e1e7 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -9,6 +9,7 @@ import { } from './canvas-drawing'; import { fitNaturalSizeToCanvas, + getClippedPasteBounds, getBounds, normalizeCanvasBounds, normalizeTextBounds, @@ -413,13 +414,24 @@ export class WebDraftEditor extends EventTarget { const before = this.captureHistorySnapshot(); const {context} = this.layerManager.activeLayer; const target = this.selectionBounds ?? this.clipboard.bounds; - const x = Math.round(target.x); - const y = Math.round(target.y); + const pasteBounds = getClippedPasteBounds(target, this.clipboard.bounds, this.canvasSize); - context.putImageData(this.clipboard.imageData, x, y); + if (!pasteBounds) { + return; + } + + context.putImageData( + this.clipboard.imageData, + pasteBounds.targetX - pasteBounds.sourceX, + pasteBounds.targetY - pasteBounds.sourceY, + pasteBounds.sourceX, + pasteBounds.sourceY, + pasteBounds.width, + pasteBounds.height, + ); this.selectionBounds = { - x, - y, + x: pasteBounds.targetX, + y: pasteBounds.targetY, width: this.clipboard.bounds.width, height: this.clipboard.bounds.height, }; diff --git a/app-v3/tests/editor-geometry.test.mjs b/app-v3/tests/editor-geometry.test.mjs index 521c8b1..eda0354 100644 --- a/app-v3/tests/editor-geometry.test.mjs +++ b/app-v3/tests/editor-geometry.test.mjs @@ -2,7 +2,8 @@ import assert from 'node:assert/strict'; import {importTypescriptModule, test} from './test-support.mjs'; const geometryModule = await importTypescriptModule('../src/core/editor-geometry.ts'); -const {fitNaturalSizeToCanvas, getBounds, normalizeCanvasBounds, normalizeTextBounds} = geometryModule; +const {fitNaturalSizeToCanvas, getBounds, getClippedPasteBounds, normalizeCanvasBounds, normalizeTextBounds} = + geometryModule; test('getBounds normalizes drag direction', () => { assert.deepEqual(getBounds({x: 80, y: 90}, {x: 20, y: 10}), { @@ -42,4 +43,26 @@ test('fitNaturalSizeToCanvas scales large images and centers them', () => { }); }); +test('getClippedPasteBounds clips pasted pixels to the canvas', () => { + assert.deepEqual(getClippedPasteBounds({x: -4, y: 8}, {width: 10, height: 6}, {width: 20, height: 20}), { + targetX: 0, + targetY: 8, + sourceX: 4, + sourceY: 0, + width: 6, + height: 6, + }); + + assert.deepEqual(getClippedPasteBounds({x: 15, y: 18}, {width: 10, height: 6}, {width: 20, height: 20}), { + targetX: 15, + targetY: 18, + sourceX: 0, + sourceY: 0, + width: 5, + height: 2, + }); + + assert.equal(getClippedPasteBounds({x: 25, y: 5}, {width: 10, height: 6}, {width: 20, height: 20}), null); +}); + console.log('Editor geometry tests passed.'); From 0d9d87b095db66d276c102688a9d8133c3c52c47 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 12:01:03 +0200 Subject: [PATCH 28/62] Add camera panel with live preview, filter controls, snapshot functionality, and integration with canvas for editing and saving --- app-v3/src/core/webdraft-editor.ts | 11 ++ app-v3/src/styles.css | 58 ++++++ app-v3/src/ui/camera-panel.ts | 279 +++++++++++++++++++++++++++++ app-v3/src/ui/toolbar-sections.ts | 13 +- 4 files changed, 351 insertions(+), 10 deletions(-) create mode 100644 app-v3/src/ui/camera-panel.ts diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 3a3e1e7..242329f 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -292,6 +292,17 @@ export class WebDraftEditor extends EventTarget { } } + importCanvasAsLayer(source: HTMLCanvasElement): void { + const before = this.captureHistorySnapshot(); + const target = fitNaturalSizeToCanvas( + {naturalWidth: source.width, naturalHeight: source.height}, + this.canvasSize, + ); + this.layerManager.drawImageOnNewLayer(source, this.state.canvasWidth, this.state.canvasHeight, target); + this.pushHistory(before, this.captureHistorySnapshot()); + this.dispatchChange(); + } + async exportPng(): Promise { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 21e92b6..cfa4ba3 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -497,3 +497,61 @@ select { flex-basis: min(252px, 86vw); } } + +.camera-panel-backdrop { + position: fixed; + inset: 0; + z-index: 59; + background: rgb(0 0 0 / 50%); +} + +.camera-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 60; + display: grid; + gap: 12px; + padding: 16px; + width: min(480px, 96vw); + background: #17191d; + border: 1px solid #343941; + border-radius: 8px; + box-shadow: 0 24px 80px rgb(0 0 0 / 60%); +} + +.camera-panel__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.camera-panel__title { + font-weight: 600; +} + +.camera-panel__close { + padding: 0 10px; + min-height: 32px; +} + +.camera-panel__canvas { + width: 100%; + aspect-ratio: 4 / 3; + background: #000; + border-radius: 4px; + display: block; +} + +.camera-panel__filters { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.camera-panel__actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} diff --git a/app-v3/src/ui/camera-panel.ts b/app-v3/src/ui/camera-panel.ts new file mode 100644 index 0000000..ed45195 --- /dev/null +++ b/app-v3/src/ui/camera-panel.ts @@ -0,0 +1,279 @@ +import type {WebDraftEditor} from '../core/webdraft-editor.ts'; +import type {StatusReporter} from './status-toasts.ts'; + +type Filters = { + sepia: boolean; + noise: boolean; + greyscale: boolean; + negative: boolean; +}; + +function applyFilters(data: Uint8ClampedArray, filters: Filters): void { + for (let i = 0; i < data.length; i += 4) { + let r = data[i]; + let g = data[i + 1]; + let b = data[i + 2]; + + if (filters.greyscale || filters.sepia) { + const avg = 0.3 * r + 0.59 * g + 0.11 * b; + r = filters.sepia ? avg + 100 : avg; + g = filters.sepia ? avg + 50 : avg; + b = filters.sepia ? avg : avg; + } + + if (filters.negative) { + r = 255 - r; + g = 255 - g; + b = 255 - b; + } + + if (filters.noise) { + const rand = (Math.random() - 0.5) * 100; + r = Math.max(0, Math.min(255, r + rand)); + g = Math.max(0, Math.min(255, g + rand)); + b = Math.max(0, Math.min(255, b + rand)); + } + + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + } +} + +function createFilterCheckbox(label: string, onChange: (checked: boolean) => void): HTMLLabelElement { + const el = document.createElement('label'); + el.className = 'checkbox-control'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.addEventListener('change', () => onChange(input.checked)); + + const text = document.createElement('span'); + text.textContent = label; + el.append(input, text); + + return el; +} + +export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): void { + const mediaDevices = navigator.mediaDevices; + + if (!mediaDevices?.getUserMedia) { + status.show('Camera is unavailable in this browser.', 'error'); + return; + } + + const filters: Filters = {sepia: false, noise: false, greyscale: false, negative: false}; + let stream: MediaStream | null = null; + let rafId = 0; + let isSnapped = false; + let snapshotCanvas: HTMLCanvasElement | null = null; + + const video = document.createElement('video'); + video.muted = true; + video.playsInline = true; + + const backdrop = document.createElement('div'); + backdrop.className = 'camera-panel-backdrop'; + + const panel = document.createElement('div'); + panel.className = 'camera-panel'; + + // Header + const header = document.createElement('div'); + header.className = 'camera-panel__header'; + + const title = document.createElement('span'); + title.className = 'camera-panel__title'; + title.textContent = 'Camera'; + + const closeBtn = document.createElement('button'); + closeBtn.type = 'button'; + closeBtn.className = 'camera-panel__close command-button'; + closeBtn.textContent = '✕'; + closeBtn.addEventListener('click', closePanel); + + header.append(title, closeBtn); + + // Preview canvas + const previewCanvas = document.createElement('canvas'); + previewCanvas.className = 'camera-panel__canvas'; + + // Filters + const filtersContainer = document.createElement('div'); + filtersContainer.className = 'camera-panel__filters'; + + const filterDefs: Array<{label: string; key: keyof Filters}> = [ + {label: 'Sepia', key: 'sepia'}, + {label: 'Noise', key: 'noise'}, + {label: 'Greyscale', key: 'greyscale'}, + {label: 'Negative', key: 'negative'}, + ]; + + for (const {label, key} of filterDefs) { + filtersContainer.append(createFilterCheckbox(label, (checked) => { + filters[key] = checked; + })); + } + + // Actions + const actionsContainer = document.createElement('div'); + actionsContainer.className = 'camera-panel__actions'; + + const snapBtn = document.createElement('button'); + snapBtn.type = 'button'; + snapBtn.className = 'command-button'; + snapBtn.textContent = 'Snap'; + snapBtn.addEventListener('click', doSnap); + + const snapToCanvasBtn = document.createElement('button'); + snapToCanvasBtn.type = 'button'; + snapToCanvasBtn.className = 'command-button'; + snapToCanvasBtn.textContent = 'Snap to canvas'; + snapToCanvasBtn.addEventListener('click', doSnapToCanvas); + + const applyBtn = document.createElement('button'); + applyBtn.type = 'button'; + applyBtn.className = 'command-button'; + applyBtn.textContent = 'Apply to canvas'; + applyBtn.addEventListener('click', doApply); + + const saveBtn = document.createElement('button'); + saveBtn.type = 'button'; + saveBtn.className = 'command-button'; + saveBtn.textContent = 'Save to file'; + saveBtn.addEventListener('click', doSave); + + const backBtn = document.createElement('button'); + backBtn.type = 'button'; + backBtn.className = 'command-button'; + backBtn.textContent = 'Back to live'; + backBtn.addEventListener('click', resumeLive); + + showLiveActions(); + + panel.append(header, previewCanvas, filtersContainer, actionsContainer); + document.body.append(backdrop, panel); + + // Keyboard: Escape closes panel + function onKeyDown(e: KeyboardEvent): void { + if (e.key === 'Escape') { + e.preventDefault(); + closePanel(); + } + } + document.addEventListener('keydown', onKeyDown); + + // Start camera + void startCamera(); + + async function startCamera(): Promise { + try { + stream = await mediaDevices.getUserMedia({video: true}); + video.srcObject = stream; + + await new Promise((resolve) => { + video.addEventListener('loadedmetadata', () => resolve(), {once: true}); + void video.play(); + }); + + const track = stream.getVideoTracks()[0]; + if (track?.label) { + title.textContent = `Camera — ${track.label}`; + } + + previewCanvas.width = video.videoWidth; + previewCanvas.height = video.videoHeight; + + startLiveLoop(); + } catch (error) { + closePanel(); + status.show(error instanceof Error ? error.message : 'Camera error.', 'error'); + } + } + + function startLiveLoop(): void { + const ctx = previewCanvas.getContext('2d'); + if (!ctx) return; + + function frame(): void { + if (isSnapped) return; + ctx!.drawImage(video, 0, 0); + const hasActiveFilter = filters.sepia || filters.noise || filters.greyscale || filters.negative; + if (hasActiveFilter) { + const imageData = ctx!.getImageData(0, 0, previewCanvas.width, previewCanvas.height); + applyFilters(imageData.data, filters); + ctx!.putImageData(imageData, 0, 0); + } + rafId = requestAnimationFrame(frame); + } + + rafId = requestAnimationFrame(frame); + } + + function doSnap(): void { + cancelAnimationFrame(rafId); + isSnapped = true; + + snapshotCanvas = document.createElement('canvas'); + snapshotCanvas.width = previewCanvas.width; + snapshotCanvas.height = previewCanvas.height; + const snapCtx = snapshotCanvas.getContext('2d'); + snapCtx?.drawImage(previewCanvas, 0, 0); + + showSnappedActions(); + } + + function resumeLive(): void { + isSnapped = false; + snapshotCanvas = null; + showLiveActions(); + startLiveLoop(); + } + + function doApply(): void { + if (!snapshotCanvas) return; + editor.importCanvasAsLayer(snapshotCanvas); + status.show('Camera frame added.', 'success'); + closePanel(); + } + + function doSave(): void { + if (!snapshotCanvas) return; + snapshotCanvas.toBlob((blob) => { + if (!blob) return; + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'camera-snap.png'; + a.click(); + URL.revokeObjectURL(url); + }); + } + + function doSnapToCanvas(): void { + const tmp = document.createElement('canvas'); + tmp.width = previewCanvas.width; + tmp.height = previewCanvas.height; + tmp.getContext('2d')?.drawImage(previewCanvas, 0, 0); + editor.importCanvasAsLayer(tmp); + status.show('Camera frame added.', 'success'); + } + + function showLiveActions(): void { + actionsContainer.replaceChildren(snapBtn, snapToCanvasBtn); + } + + function showSnappedActions(): void { + actionsContainer.replaceChildren(applyBtn, saveBtn, backBtn); + } + + function closePanel(): void { + cancelAnimationFrame(rafId); + document.removeEventListener('keydown', onKeyDown); + stream?.getTracks().forEach((t) => t.stop()); + video.srcObject = null; + panel.remove(); + backdrop.remove(); + } +} diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index b4aa6a0..fe230ed 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -1,5 +1,6 @@ import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; +import {openCameraPanel} from './camera-panel'; import {createColorPicker} from './color-picker'; import type {StatusReporter} from './status-toasts'; import {getErrorMessage} from './status-toasts'; @@ -286,8 +287,8 @@ export function createFileSection( status: StatusReporter, ): ToolbarSection { const uploadButton = createCommandButton('Upload image', () => fileInput.click()); - const cameraButton = createCommandButton('Camera snap', () => { - void importCameraFrame(editor, status); + const cameraButton = createCommandButton('Camera', () => { + openCameraPanel(editor, status); }); const exportButton = createCommandButton('Export PNG', () => { void exportImage(); @@ -299,14 +300,6 @@ export function createFileSection( }; } -async function importCameraFrame(editor: WebDraftEditor, status: StatusReporter): Promise { - try { - await editor.importCameraFrame(); - status.show('Camera frame added.', 'success'); - } catch (error) { - status.show(getErrorMessage(error), 'error'); - } -} export function syncToolbarSections(sections: ToolbarSection[]): void { for (const section of sections) { From f4bd6547c20be3327faf3fb2a292e04c38ebf551 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 17:58:26 +0200 Subject: [PATCH 29/62] Update tool configurations to use SVG icons, add `createSvgIcon` utility, and enhance button accessibility and layout --- app-v3/src/styles.css | 9 ++++ app-v3/src/ui/toolbar-sections.ts | 71 ++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index cfa4ba3..9affa68 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -162,10 +162,19 @@ select { } .tool-button { + display: flex; + align-items: center; + justify-content: center; width: 100%; height: 44px; } +.tool-button svg, +.command-button svg { + pointer-events: none; + flex-shrink: 0; +} + .tool-button:hover, .command-button:hover { border-color: #5b9cff; diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index fe230ed..d6d3231 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -26,17 +26,68 @@ type ToolConfig = { }; const tools: ToolConfig[] = [ - {id: Tool.Select, label: 'Select', icon: 'S'}, - {id: Tool.Pencil, label: 'Pencil', icon: 'P'}, - {id: Tool.Eraser, label: 'Eraser', icon: 'E'}, - {id: Tool.Sampler, label: 'Sampler', icon: 'C'}, - {id: Tool.FillBucket, label: 'Fill bucket', icon: 'B'}, - {id: Tool.Web, label: 'Web', icon: 'W'}, - {id: Tool.Rectangle, label: 'Rectangle', icon: 'R'}, - {id: Tool.Ellipse, label: 'Ellipse', icon: 'O'}, - {id: Tool.Text, label: 'Text', icon: 'T'}, + { + id: Tool.Select, + label: 'Select — S', + icon: '', + }, + { + id: Tool.Pencil, + label: 'Pencil — P', + icon: '', + }, + { + id: Tool.Eraser, + label: 'Eraser — E', + icon: '', + }, + { + id: Tool.Sampler, + label: 'Sampler — C', + icon: '', + }, + { + id: Tool.FillBucket, + label: 'Fill bucket — B', + icon: '', + }, + { + id: Tool.Web, + label: 'Web — W', + icon: '', + }, + { + id: Tool.Rectangle, + label: 'Rectangle — R', + icon: '', + }, + { + id: Tool.Ellipse, + label: 'Ellipse — O', + icon: '', + }, + { + id: Tool.Text, + label: 'Text — T', + icon: '', + }, ]; +function createSvgIcon(content: string): SVGSVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('width', '18'); + svg.setAttribute('height', '18'); + svg.setAttribute('fill', 'none'); + svg.setAttribute('stroke', 'currentColor'); + svg.setAttribute('stroke-width', '2'); + svg.setAttribute('stroke-linecap', 'round'); + svg.setAttribute('stroke-linejoin', 'round'); + svg.setAttribute('aria-hidden', 'true'); + svg.innerHTML = content; + return svg; +} + export function createToolSection(editor: WebDraftEditor): ToolbarSection { const toolGroup = document.createElement('div'); toolGroup.className = 'toolbar__group'; @@ -49,7 +100,7 @@ export function createToolSection(editor: WebDraftEditor): ToolbarSection { button.className = 'tool-button'; button.dataset.tool = tool.id; button.title = tool.label; - button.textContent = tool.icon; + button.append(createSvgIcon(tool.icon)); button.addEventListener('click', () => { editor.setTool(tool.id); }); From 6ebbb21ce34a9eb888bbf7f156ad2e322dc9f305 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 18:43:40 +0200 Subject: [PATCH 30/62] Improve UI responsiveness by adding dynamic visibility controls for toolbar elements and hiding overflow on the body --- app-v3/src/styles.css | 5 ++++ app-v3/src/ui/toolbar-sections.ts | 39 ++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 9affa68..2276813 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -8,10 +8,15 @@ box-sizing: border-box; } +[hidden] { + display: none !important; +} + body { margin: 0; min-width: 320px; min-height: 100vh; + overflow: hidden; background: #202327; } diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index d6d3231..f0837b8 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -168,6 +168,22 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { onChange: (value) => editor.setFillTolerance(value), }); + const syncVisibility = () => { + const tool = editor.state.activeTool; + const isShape = tool === Tool.Rectangle || tool === Tool.Ellipse; + const isBucket = tool === Tool.FillBucket; + const isWeb = tool === Tool.Web; + const hasBrushSize = tool === Tool.Pencil || tool === Tool.Eraser; + fillToggle.element.hidden = !isShape; + fillColorPicker.hidden = !isShape; + fillOpacityControl.element.hidden = !isShape; + fillToleranceControl.element.hidden = !isBucket; + webSensitivityControl.element.hidden = !isWeb; + sizeControl.element.hidden = !hasBrushSize && !isWeb && !isShape; + }; + + syncVisibility(); + return { element: createToolbarSection( colorPicker, @@ -179,6 +195,7 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { webSensitivityControl.element, ), sync: () => { + syncVisibility(); colorPicker.setValue(editor.state.color); fillToggle.setChecked(editor.state.fillEnabled); fillColorPicker.setValue(editor.state.fillColor); @@ -225,6 +242,16 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { onChange: (value) => editor.setShadowOffsetY(value), }); + const syncShadowDetails = () => { + const visible = editor.state.shadowEnabled; + shadowColorPicker.hidden = !visible; + shadowBlurControl.element.hidden = !visible; + shadowXControl.element.hidden = !visible; + shadowYControl.element.hidden = !visible; + }; + + syncShadowDetails(); + return { element: createToolbarSection( shadowToggle.element, @@ -234,6 +261,7 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { shadowYControl.element, ), sync: () => { + syncShadowDetails(); shadowToggle.setChecked(editor.state.shadowEnabled); shadowColorPicker.setValue(editor.state.shadowColor); shadowBlurControl.setValue(editor.state.shadowBlur); @@ -259,11 +287,16 @@ export function createTextSection(editor: WebDraftEditor): ToolbarSection { editor.setTextItalic(enabled); }); + const element = createToolbarSection( + createGrid('field-grid', fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element), + ); + + element.hidden = editor.state.activeTool !== Tool.Text; + return { - element: createToolbarSection( - createGrid('field-grid', fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element), - ), + element, sync: () => { + element.hidden = editor.state.activeTool !== Tool.Text; fontSelect.value = editor.state.textFontFamily; alignSelect.value = editor.state.textAlign; textBoldToggle.setChecked(editor.state.textBold); From 1ce7111531a6e7971c529f9491299777f5334989 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 20:48:37 +0200 Subject: [PATCH 31/62] Add right panel with toolbar for layer controls, update layout to accommodate new panel and enhance edit buttons with SVG icons and tooltips --- app-v3/src/main.ts | 11 +++- app-v3/src/styles.css | 24 ++++++++- app-v3/src/ui/toolbar-sections.ts | 83 ++++++++++++++++++++++++------- app-v3/src/ui/toolbar.ts | 6 --- 4 files changed, 97 insertions(+), 27 deletions(-) diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index ecb4bec..93efcde 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -1,6 +1,8 @@ import { WebDraftEditor } from './core/webdraft-editor'; +import { createLayersPanel } from './ui/layers-panel'; import { createStatusToasts } from './ui/status-toasts'; import { createToolbar } from './ui/toolbar'; +import { createToolbarSection } from './ui/toolbar-controls'; import './styles.css'; const app = document.querySelector('#app'); @@ -21,10 +23,13 @@ workspace.className = 'workspace'; const surface = document.createElement('div'); surface.className = 'canvas-surface'; +const rightPanel = document.createElement('aside'); +rightPanel.className = 'right-panel'; + const statusToasts = createStatusToasts(); workspace.append(surface); -shell.append(sidebar, workspace); +shell.append(sidebar, workspace, rightPanel); app.append(shell, statusToasts); const editor = new WebDraftEditor(surface, { @@ -35,4 +40,8 @@ const editor = new WebDraftEditor(surface, { }); sidebar.append(createToolbar(editor, statusToasts)); +const rightToolbar = document.createElement('div'); +rightToolbar.className = 'toolbar'; +rightToolbar.append(createToolbarSection(createLayersPanel(editor))); +rightPanel.append(rightToolbar); editor.mount(); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 2276813..ed4ccb3 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -39,7 +39,7 @@ select { .app-shell { display: grid; - grid-template-columns: 240px minmax(0, 1fr); + grid-template-columns: 240px minmax(0, 1fr) 240px; width: 100%; min-width: 0; min-height: 100vh; @@ -55,6 +55,15 @@ select { background: #17191d; } +.right-panel { + min-width: 0; + min-height: 100vh; + max-height: 100vh; + overflow: auto; + border-left: 1px solid #343941; + background: #17191d; +} + .workspace { display: grid; place-items: center; @@ -253,6 +262,10 @@ select { } .command-button { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; width: 100%; padding: 0 12px; } @@ -447,7 +460,7 @@ select { @media (max-width: 760px) { .app-shell { grid-template-columns: 1fr; - grid-template-rows: auto minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; min-height: 100svh; } @@ -460,6 +473,13 @@ select { border-bottom: 1px solid #343941; } + .right-panel { + min-height: 0; + max-height: none; + border-left: 0; + border-top: 1px solid #343941; + } + .toolbar { display: flex; align-items: center; diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index f0837b8..bb6daa7 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -305,25 +305,72 @@ export function createTextSection(editor: WebDraftEditor): ToolbarSection { }; } +function createEditButton(title: string, svgContent: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'command-button'; + button.title = title; + button.append(createSvgIcon(svgContent)); + button.addEventListener('click', onClick); + return button; +} + export function createEditSection(editor: WebDraftEditor): ToolbarSection { - const clearButton = createCommandButton('Clear', () => editor.clear()); - const undoButton = createCommandButton('Undo', () => editor.undo()); - const redoButton = createCommandButton('Redo', () => editor.redo()); - const copyButton = createCommandButton('Copy', () => editor.copySelection()); - const cutButton = createCommandButton('Cut', () => editor.cutSelection()); - const pasteButton = createCommandButton('Paste', () => editor.pasteSelection()); - - const invertButton = createCommandButton('Invert', () => editor.invertActiveLayer()); - const rotateLeftButton = createCommandButton('Rotate L', () => editor.rotateActiveLayer('left')); - const rotateRightButton = createCommandButton('Rotate R', () => editor.rotateActiveLayer('right')); - const mirrorHorizontalButton = createCommandButton('Mirror H', () => editor.mirrorActiveLayer('horizontal')); - const mirrorVerticalButton = createCommandButton('Mirror V', () => editor.mirrorActiveLayer('vertical')); - - invertButton.title = 'Invert colors'; - rotateLeftButton.title = 'Rotate left'; - rotateRightButton.title = 'Rotate right'; - mirrorHorizontalButton.title = 'Mirror horizontally'; - mirrorVerticalButton.title = 'Mirror vertically'; + const clearButton = createEditButton( + 'Clear', + '', + () => editor.clear(), + ); + const undoButton = createEditButton( + 'Undo — Ctrl+Z', + '', + () => editor.undo(), + ); + const redoButton = createEditButton( + 'Redo — Ctrl+Shift+Z', + '', + () => editor.redo(), + ); + const copyButton = createEditButton( + 'Copy — Ctrl+C', + '', + () => editor.copySelection(), + ); + const cutButton = createEditButton( + 'Cut — Ctrl+X', + '', + () => editor.cutSelection(), + ); + const pasteButton = createEditButton( + 'Paste — Ctrl+V', + '', + () => editor.pasteSelection(), + ); + const invertButton = createEditButton( + 'Invert colors — I', + '', + () => editor.invertActiveLayer(), + ); + const rotateLeftButton = createEditButton( + 'Rotate left — ,', + '', + () => editor.rotateActiveLayer('left'), + ); + const rotateRightButton = createEditButton( + 'Rotate right — .', + '', + () => editor.rotateActiveLayer('right'), + ); + const mirrorHorizontalButton = createEditButton( + 'Mirror horizontally — H', + '', + () => editor.mirrorActiveLayer('horizontal'), + ); + const mirrorVerticalButton = createEditButton( + 'Mirror vertically — V', + '', + () => editor.mirrorActiveLayer('vertical'), + ); return { element: createToolbarSection( diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 87d8e8f..292504e 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,6 +1,5 @@ import type {WebDraftEditor} from '../core/webdraft-editor'; import {bindKeyboardShortcuts} from './keyboard-shortcuts'; -import {createLayersPanel} from './layers-panel'; import type {StatusReporter} from './status-toasts'; import {getErrorMessage} from './status-toasts'; import { @@ -13,7 +12,6 @@ import { createToolSection, syncToolbarSections, } from './toolbar-sections'; -import {createToolbarSection} from './toolbar-controls'; export function createToolbar(editor: WebDraftEditor, status: StatusReporter): HTMLElement { const toolbar = document.createElement('div'); @@ -47,10 +45,6 @@ export function createToolbar(editor: WebDraftEditor, status: StatusReporter): H createEditSection(editor), createResizeSection(editor, status), createFileSection(editor, fileInput, exportImage, status), - { - element: createToolbarSection(createLayersPanel(editor)), - sync: () => {}, - }, ]; editor.addEventListener('change', () => syncToolbarSections(sections)); From 159f637b24f6457b32058882fd8e195e1361b341 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 21:59:37 +0200 Subject: [PATCH 32/62] Refactor right panel creation into `createRightPanel`, consolidate toolbar sections, and improve button layout with SVG-based controls. --- app-v3/src/main.ts | 8 +-- app-v3/src/ui/right-panel.ts | 24 +++++++++ app-v3/src/ui/toolbar-sections.ts | 85 +++++++++++++++---------------- app-v3/src/ui/toolbar.ts | 2 - 4 files changed, 66 insertions(+), 53 deletions(-) create mode 100644 app-v3/src/ui/right-panel.ts diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index 93efcde..603cc7b 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -1,8 +1,7 @@ import { WebDraftEditor } from './core/webdraft-editor'; -import { createLayersPanel } from './ui/layers-panel'; +import { createRightPanel } from './ui/right-panel'; import { createStatusToasts } from './ui/status-toasts'; import { createToolbar } from './ui/toolbar'; -import { createToolbarSection } from './ui/toolbar-controls'; import './styles.css'; const app = document.querySelector('#app'); @@ -40,8 +39,5 @@ const editor = new WebDraftEditor(surface, { }); sidebar.append(createToolbar(editor, statusToasts)); -const rightToolbar = document.createElement('div'); -rightToolbar.className = 'toolbar'; -rightToolbar.append(createToolbarSection(createLayersPanel(editor))); -rightPanel.append(rightToolbar); +rightPanel.append(createRightPanel(editor)); editor.mount(); diff --git a/app-v3/src/ui/right-panel.ts b/app-v3/src/ui/right-panel.ts new file mode 100644 index 0000000..cf638d7 --- /dev/null +++ b/app-v3/src/ui/right-panel.ts @@ -0,0 +1,24 @@ +import type {WebDraftEditor} from '../core/webdraft-editor'; +import {createLayersPanel} from './layers-panel'; +import {createEditSection, syncToolbarSections} from './toolbar-sections'; +import {createToolbarSection} from './toolbar-controls'; + +export function createRightPanel(editor: WebDraftEditor): HTMLElement { + const panel = document.createElement('div'); + panel.className = 'toolbar'; + + const sections = [ + createEditSection(editor), + { + element: createToolbarSection(createLayersPanel(editor)), + sync: () => {}, + }, + ]; + + editor.addEventListener('change', () => syncToolbarSections(sections)); + syncToolbarSections(sections); + + panel.append(...sections.map((s) => s.element)); + + return panel; +} diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index bb6daa7..8970654 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -88,6 +88,16 @@ function createSvgIcon(content: string): SVGSVGElement { return svg; } +function createEditButton(title: string, svgContent: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'command-button'; + button.title = title; + button.append(createSvgIcon(svgContent)); + button.addEventListener('click', onClick); + return button; +} + export function createToolSection(editor: WebDraftEditor): ToolbarSection { const toolGroup = document.createElement('div'); toolGroup.className = 'toolbar__group'; @@ -109,8 +119,37 @@ export function createToolSection(editor: WebDraftEditor): ToolbarSection { toolGroup.append(button); } + const transformGroup = createGrid( + 'toolbar__group', + createEditButton( + 'Invert colors — I', + '', + () => editor.invertActiveLayer(), + ), + createEditButton( + 'Rotate left — ,', + '', + () => editor.rotateActiveLayer('left'), + ), + createEditButton( + 'Rotate right — .', + '', + () => editor.rotateActiveLayer('right'), + ), + createEditButton( + 'Mirror horizontally — H', + '', + () => editor.mirrorActiveLayer('horizontal'), + ), + createEditButton( + 'Mirror vertically — V', + '', + () => editor.mirrorActiveLayer('vertical'), + ), + ); + return { - element: createToolbarSection(toolGroup), + element: createToolbarSection(toolGroup, transformGroup), sync: () => { for (const [tool, button] of toolButtons) { button.classList.toggle('is-active', tool === editor.state.activeTool); @@ -305,16 +344,6 @@ export function createTextSection(editor: WebDraftEditor): ToolbarSection { }; } -function createEditButton(title: string, svgContent: string, onClick: () => void): HTMLButtonElement { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'command-button'; - button.title = title; - button.append(createSvgIcon(svgContent)); - button.addEventListener('click', onClick); - return button; -} - export function createEditSection(editor: WebDraftEditor): ToolbarSection { const clearButton = createEditButton( 'Clear', @@ -346,43 +375,9 @@ export function createEditSection(editor: WebDraftEditor): ToolbarSection { '', () => editor.pasteSelection(), ); - const invertButton = createEditButton( - 'Invert colors — I', - '', - () => editor.invertActiveLayer(), - ); - const rotateLeftButton = createEditButton( - 'Rotate left — ,', - '', - () => editor.rotateActiveLayer('left'), - ); - const rotateRightButton = createEditButton( - 'Rotate right — .', - '', - () => editor.rotateActiveLayer('right'), - ); - const mirrorHorizontalButton = createEditButton( - 'Mirror horizontally — H', - '', - () => editor.mirrorActiveLayer('horizontal'), - ); - const mirrorVerticalButton = createEditButton( - 'Mirror vertically — V', - '', - () => editor.mirrorActiveLayer('vertical'), - ); - return { element: createToolbarSection( createGrid('command-grid', clearButton, undoButton, redoButton, copyButton, cutButton, pasteButton), - createGrid( - 'command-grid', - invertButton, - rotateLeftButton, - rotateRightButton, - mirrorHorizontalButton, - mirrorVerticalButton, - ), ), sync: () => { undoButton.disabled = !editor.canUndo; diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 292504e..f095fe8 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -3,7 +3,6 @@ import {bindKeyboardShortcuts} from './keyboard-shortcuts'; import type {StatusReporter} from './status-toasts'; import {getErrorMessage} from './status-toasts'; import { - createEditSection, createFileSection, createResizeSection, createShadowSection, @@ -42,7 +41,6 @@ export function createToolbar(editor: WebDraftEditor, status: StatusReporter): H createStyleSection(editor), createShadowSection(editor), createTextSection(editor), - createEditSection(editor), createResizeSection(editor, status), createFileSection(editor, fileInput, exportImage, status), ]; From e4d9c64ac80fe9275684a7ee0298ed0d70364fc0 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Tue, 9 Jun 2026 22:12:01 +0200 Subject: [PATCH 33/62] Add `.wdraft` project format support with save and open functionality, parser validation, and toolbar integration. Update documentation with format details. --- .idea/dictionaries/project.xml | 9 +++ app-v3/src/core/project-file.ts | 95 ++++++++++++++++++++++++++++++ app-v3/src/core/webdraft-editor.ts | 68 +++++++++++++++++++++ app-v3/src/main.ts | 2 +- app-v3/src/ui/right-panel.ts | 6 +- app-v3/src/ui/toolbar-sections.ts | 51 ++++++++++++++++ docs/wdraft-format.md | 84 ++++++++++++++++++++++++++ 7 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 .idea/dictionaries/project.xml create mode 100644 app-v3/src/core/project-file.ts create mode 100644 docs/wdraft-format.md diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..55e8f10 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,9 @@ + + + + WDRAFT + Wdraft + wdraft + + + diff --git a/app-v3/src/core/project-file.ts b/app-v3/src/core/project-file.ts new file mode 100644 index 0000000..52f1189 --- /dev/null +++ b/app-v3/src/core/project-file.ts @@ -0,0 +1,95 @@ +export const WDRAFT_VERSION = 1; +export const WDRAFT_MIME_TYPE = 'application/x-webdraft'; +export const WDRAFT_EXTENSION = '.wdraft'; + +export type WdraftLayer = { + id: string; + name: string; + visible: boolean; + width: number; + height: number; + imageDataUrl: string; +}; + +export type WdraftFile = { + version: number; + canvasWidth: number; + canvasHeight: number; + activeLayerId: string; + layerCount: number; + layers: WdraftLayer[]; +}; + +export function parseWdraftFile(text: string): WdraftFile { + let data: unknown; + + try { + data = JSON.parse(text); + } catch { + throw new Error('Invalid .wdraft file: not valid JSON.'); + } + + if (typeof data !== 'object' || data === null) { + throw new Error('Invalid .wdraft file: expected an object.'); + } + + const obj = data as Record; + + if (typeof obj['version'] !== 'number') { + throw new Error('Invalid .wdraft file: missing version.'); + } + + if (obj['version'] !== WDRAFT_VERSION) { + throw new Error(`Unsupported .wdraft version: ${obj['version']}. Expected ${WDRAFT_VERSION}.`); + } + + if (typeof obj['canvasWidth'] !== 'number' || typeof obj['canvasHeight'] !== 'number') { + throw new Error('Invalid .wdraft file: missing canvasWidth or canvasHeight.'); + } + + if (typeof obj['activeLayerId'] !== 'string') { + throw new Error('Invalid .wdraft file: missing activeLayerId.'); + } + + if (typeof obj['layerCount'] !== 'number') { + throw new Error('Invalid .wdraft file: missing layerCount.'); + } + + if (!Array.isArray(obj['layers']) || obj['layers'].length === 0) { + throw new Error('Invalid .wdraft file: layers must be a non-empty array.'); + } + + const layers: WdraftLayer[] = (obj['layers'] as unknown[]).map((item, index) => { + if (typeof item !== 'object' || item === null) { + throw new Error(`Invalid .wdraft file: layer ${index} is not an object.`); + } + const layer = item as Record; + if ( + typeof layer['id'] !== 'string' || + typeof layer['name'] !== 'string' || + typeof layer['visible'] !== 'boolean' || + typeof layer['width'] !== 'number' || + typeof layer['height'] !== 'number' || + typeof layer['imageDataUrl'] !== 'string' + ) { + throw new Error(`Invalid .wdraft file: layer ${index} is missing required fields.`); + } + return { + id: layer['id'], + name: layer['name'], + visible: layer['visible'], + width: layer['width'], + height: layer['height'], + imageDataUrl: layer['imageDataUrl'], + }; + }); + + return { + version: obj['version'], + canvasWidth: obj['canvasWidth'], + canvasHeight: obj['canvasHeight'], + activeLayerId: obj['activeLayerId'], + layerCount: obj['layerCount'], + layers, + }; +} diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 242329f..670c430 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,4 +1,5 @@ import type {LayerDocumentSnapshot, LayerSummary} from './layer-manager'; +import {WDRAFT_MIME_TYPE, WDRAFT_VERSION, type WdraftFile, type WdraftLayer} from './project-file'; import { drawLine, drawPoint, @@ -303,6 +304,57 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + async exportProject(): Promise { + const snapshot = this.layerManager.captureDocument(); + const layers: WdraftLayer[] = snapshot.layers.map((layer) => { + const canvas = document.createElement('canvas'); + canvas.width = layer.imageData.width; + canvas.height = layer.imageData.height; + canvas.getContext('2d')!.putImageData(layer.imageData, 0, 0); + return { + id: layer.layerId, + name: layer.name, + visible: layer.visible, + width: layer.imageData.width, + height: layer.imageData.height, + imageDataUrl: canvas.toDataURL('image/png'), + }; + }); + const file: WdraftFile = { + version: WDRAFT_VERSION, + canvasWidth: this.state.canvasWidth, + canvasHeight: this.state.canvasHeight, + activeLayerId: snapshot.activeLayerId, + layerCount: snapshot.layerCount, + layers, + }; + return new Blob([JSON.stringify(file)], {type: WDRAFT_MIME_TYPE}); + } + + async importProject(file: WdraftFile): Promise { + const before = this.captureHistorySnapshot(); + const imageDataList = await Promise.all( + file.layers.map((l) => loadDataUrlAsImageData(l.imageDataUrl, l.width, l.height)), + ); + const newDocument = { + activeLayerId: file.activeLayerId, + layerCount: file.layerCount, + layers: file.layers.map((l, i) => ({ + layerId: l.id, + name: l.name, + visible: l.visible, + imageData: imageDataList[i], + })), + }; + this.state.canvasWidth = file.canvasWidth; + this.state.canvasHeight = file.canvasHeight; + this.layerManager.restoreDocument(newDocument); + this.applyCanvasSize(); + this.clearSelection(); + this.pushHistory(before, this.captureHistorySnapshot()); + this.dispatchChange(); + } + async exportPng(): Promise { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -960,3 +1012,19 @@ function createImageData(buffer: {data: Uint8ClampedArray; width: number; height return new ImageData(data, buffer.width, buffer.height); } + +function loadDataUrlAsImageData(dataUrl: string, width: number, height: number): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(img, 0, 0); + resolve(ctx.getImageData(0, 0, width, height)); + }; + img.onerror = () => reject(new Error('Failed to load layer image.')); + img.src = dataUrl; + }); +} diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index 603cc7b..abde705 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -39,5 +39,5 @@ const editor = new WebDraftEditor(surface, { }); sidebar.append(createToolbar(editor, statusToasts)); -rightPanel.append(createRightPanel(editor)); +rightPanel.append(createRightPanel(editor, statusToasts)); editor.mount(); diff --git a/app-v3/src/ui/right-panel.ts b/app-v3/src/ui/right-panel.ts index cf638d7..692e417 100644 --- a/app-v3/src/ui/right-panel.ts +++ b/app-v3/src/ui/right-panel.ts @@ -1,13 +1,15 @@ import type {WebDraftEditor} from '../core/webdraft-editor'; import {createLayersPanel} from './layers-panel'; -import {createEditSection, syncToolbarSections} from './toolbar-sections'; +import type {StatusReporter} from './status-toasts'; +import {createEditSection, createProjectSection, syncToolbarSections} from './toolbar-sections'; import {createToolbarSection} from './toolbar-controls'; -export function createRightPanel(editor: WebDraftEditor): HTMLElement { +export function createRightPanel(editor: WebDraftEditor, status: StatusReporter): HTMLElement { const panel = document.createElement('div'); panel.className = 'toolbar'; const sections = [ + createProjectSection(editor, status), createEditSection(editor), { element: createToolbarSection(createLayersPanel(editor)), diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 8970654..2488fac 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -1,3 +1,4 @@ +import {parseWdraftFile} from '../core/project-file'; import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {openCameraPanel} from './camera-panel'; @@ -427,6 +428,56 @@ export function createFileSection( } +export function createProjectSection(editor: WebDraftEditor, status: StatusReporter): ToolbarSection { + const saveButton = createCommandButton('Save project', () => { + void (async () => { + try { + const blob = await editor.exportProject(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'project.wdraft'; + link.click(); + URL.revokeObjectURL(url); + status.show('Project saved.', 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } + })(); + }); + + const wdraftInput = document.createElement('input'); + wdraftInput.type = 'file'; + wdraftInput.accept = '.wdraft'; + wdraftInput.className = 'visually-hidden'; + wdraftInput.addEventListener('change', () => { + const file = wdraftInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + void (async () => { + try { + const parsed = parseWdraftFile(reader.result as string); + await editor.importProject(parsed); + status.show('Project opened.', 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } finally { + wdraftInput.value = ''; + } + })(); + }; + reader.readAsText(file); + }); + + const openButton = createCommandButton('Open project', () => wdraftInput.click()); + + return { + element: createToolbarSection(createGrid('toolbar__stack', saveButton, openButton, wdraftInput)), + sync: () => {}, + }; +} + export function syncToolbarSections(sections: ToolbarSection[]): void { for (const section of sections) { section.sync(); diff --git a/docs/wdraft-format.md b/docs/wdraft-format.md new file mode 100644 index 0000000..3a9fa12 --- /dev/null +++ b/docs/wdraft-format.md @@ -0,0 +1,84 @@ +# `.wdraft` File Format + +## Overview + +`.wdraft` is the native project format for WebDraft. It preserves all layers with their full pixel data, canvas dimensions, layer order, visibility, and the active layer — allowing work to be resumed exactly as it was left. + +- **Extension:** `.wdraft` +- **MIME type:** `application/x-webdraft` +- **Encoding:** UTF-8 JSON + +--- + +## Schema + +```json +{ + "version": 1, + "canvasWidth": 900, + "canvasHeight": 620, + "activeLayerId": "layer-3", + "layerCount": 5, + "layers": [ + { + "id": "layer-1", + "name": "Background", + "visible": true, + "width": 900, + "height": 620, + "imageDataUrl": "data:image/png;base64,..." + } + ] +} +``` + +### Top-level fields + +| Field | Type | Description | +|---|---|---| +| `version` | `number` | Format version. Currently `1`. | +| `canvasWidth` | `number` | Canvas width in pixels. | +| `canvasHeight` | `number` | Canvas height in pixels. | +| `activeLayerId` | `string` | ID of the layer that was active at save time. | +| `layerCount` | `number` | Internal counter used to generate unique layer IDs after import. Must be ≥ the highest layer index. | +| `layers` | `WdraftLayer[]` | Ordered array of layers, bottom to top. | + +### Layer fields (`WdraftLayer`) + +| Field | Type | Description | +|---|---|---| +| `id` | `string` | Unique layer identifier (e.g. `"layer-1"`). | +| `name` | `string` | Display name shown in the layers panel. | +| `visible` | `boolean` | Whether the layer is visible when compositing. | +| `width` | `number` | Width of the layer canvas in pixels. | +| `height` | `number` | Height of the layer canvas in pixels. | +| `imageDataUrl` | `string` | PNG encoded as a base64 data URL (`data:image/png;base64,...`). | + +--- + +## File size + +Each layer's pixel data is stored as a lossless PNG base64 data URL. Approximate sizes: + +| Canvas size | Layers | Estimated file size | +|---|---|---| +| 900 × 620 | 1 | ~300 KB | +| 900 × 620 | 5 | ~1.5 MB | +| 1920 × 1080 | 5 | ~5–10 MB | + +Mostly-transparent layers compress very well; fully painted layers are larger. + +--- + +## Versioning and compatibility + +- The `version` field is checked on import. Files with a version other than `1` are rejected with an error. +- Future versions will increment this number and may add new fields. Parsers should reject unknown versions rather than silently ignore unrecognised fields. + +--- + +## Usage in WebDraft + +**Save project** — serialises all layers to a `.wdraft` file and downloads it. + +**Open project** — reads a `.wdraft` file, restores canvas size and all layers, and pushes a history entry so the action can be undone. From ee656e00d9305688495ee3bf3b597a829a7eadbc Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 08:12:36 +0200 Subject: [PATCH 34/62] Switch `.wdraft` format to binary encoding (v2), replacing JSON; update save/open logic, parser, and documentation accordingly. --- app-v3/src/core/project-file.ts | 220 +++++++++++++++++++++-------- app-v3/src/core/webdraft-editor.ts | 74 ++++++---- app-v3/src/ui/toolbar-sections.ts | 6 +- docs/wdraft-format.md | 108 +++++++------- 4 files changed, 265 insertions(+), 143 deletions(-) diff --git a/app-v3/src/core/project-file.ts b/app-v3/src/core/project-file.ts index 52f1189..d9aaa03 100644 --- a/app-v3/src/core/project-file.ts +++ b/app-v3/src/core/project-file.ts @@ -1,95 +1,201 @@ -export const WDRAFT_VERSION = 1; +export const WDRAFT_VERSION = 2; +export const WDRAFT_MAGIC = [0x57, 0x44, 0x46, 0x54] as const; // "WDFT" export const WDRAFT_MIME_TYPE = 'application/x-webdraft'; export const WDRAFT_EXTENSION = '.wdraft'; -export type WdraftLayer = { +export type WdraftLayerMeta = { id: string; name: string; visible: boolean; width: number; height: number; - imageDataUrl: string; }; -export type WdraftFile = { +export type WdraftFileMeta = { version: number; canvasWidth: number; canvasHeight: number; activeLayerId: string; layerCount: number; - layers: WdraftLayer[]; + layers: WdraftLayerMeta[]; }; -export function parseWdraftFile(text: string): WdraftFile { - let data: unknown; +export type ParsedWdraftFile = { + meta: WdraftFileMeta; + pngBuffers: Uint8Array[]; +}; - try { - data = JSON.parse(text); - } catch { - throw new Error('Invalid .wdraft file: not valid JSON.'); - } +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +export function serializeWdraftBinary(meta: WdraftFileMeta, pngBuffers: Uint8Array[]): Blob { + const activeLayerIdBytes = enc.encode(meta.activeLayerId); + const layerStrings = meta.layers.map((l) => ({ + id: enc.encode(l.id), + name: enc.encode(l.name), + })); + + // Calculate total size + let size = + 4 + // magic + 1 + // version + 4 + // canvasWidth + 4 + // canvasHeight + 4 + // layerCount + 2 + activeLayerIdBytes.length + // activeLayerId + 2; // layer entries count - if (typeof data !== 'object' || data === null) { - throw new Error('Invalid .wdraft file: expected an object.'); + for (let i = 0; i < meta.layers.length; i++) { + size += + 2 + layerStrings[i].id.length + + 2 + layerStrings[i].name.length + + 1 + // visible + 4 + // width + 4 + // height + 4 + pngBuffers[i].length; // png data length + data } - const obj = data as Record; + const buffer = new ArrayBuffer(size); + const view = new DataView(buffer); + const bytes = new Uint8Array(buffer); - if (typeof obj['version'] !== 'number') { - throw new Error('Invalid .wdraft file: missing version.'); + let offset = 0; + + // Magic + for (const byte of WDRAFT_MAGIC) { + bytes[offset++] = byte; } - if (obj['version'] !== WDRAFT_VERSION) { - throw new Error(`Unsupported .wdraft version: ${obj['version']}. Expected ${WDRAFT_VERSION}.`); + // Version + bytes[offset++] = WDRAFT_VERSION; + + // Canvas dimensions + view.setUint32(offset, meta.canvasWidth, true); + offset += 4; + view.setUint32(offset, meta.canvasHeight, true); + offset += 4; + + // Internal layer counter + view.setUint32(offset, meta.layerCount, true); + offset += 4; + + // Active layer ID + view.setUint16(offset, activeLayerIdBytes.length, true); + offset += 2; + bytes.set(activeLayerIdBytes, offset); + offset += activeLayerIdBytes.length; + + // Layer entries count + view.setUint16(offset, meta.layers.length, true); + offset += 2; + + // Layers + for (let i = 0; i < meta.layers.length; i++) { + const layer = meta.layers[i]; + const idBytes = layerStrings[i].id; + const nameBytes = layerStrings[i].name; + const png = pngBuffers[i]; + + view.setUint16(offset, idBytes.length, true); + offset += 2; + bytes.set(idBytes, offset); + offset += idBytes.length; + + view.setUint16(offset, nameBytes.length, true); + offset += 2; + bytes.set(nameBytes, offset); + offset += nameBytes.length; + + bytes[offset++] = layer.visible ? 1 : 0; + + view.setUint32(offset, layer.width, true); + offset += 4; + view.setUint32(offset, layer.height, true); + offset += 4; + + view.setUint32(offset, png.length, true); + offset += 4; + bytes.set(png, offset); + offset += png.length; } - if (typeof obj['canvasWidth'] !== 'number' || typeof obj['canvasHeight'] !== 'number') { - throw new Error('Invalid .wdraft file: missing canvasWidth or canvasHeight.'); + return new Blob([buffer], {type: WDRAFT_MIME_TYPE}); +} + +export function parseWdraftBinary(buffer: ArrayBuffer): ParsedWdraftFile { + const view = new DataView(buffer); + const bytes = new Uint8Array(buffer); + + if (buffer.byteLength < 5) { + throw new Error('Invalid .wdraft file: too short.'); } - if (typeof obj['activeLayerId'] !== 'string') { - throw new Error('Invalid .wdraft file: missing activeLayerId.'); + // Magic + for (let i = 0; i < WDRAFT_MAGIC.length; i++) { + if (bytes[i] !== WDRAFT_MAGIC[i]) { + throw new Error('Invalid .wdraft file: wrong magic bytes.'); + } } - if (typeof obj['layerCount'] !== 'number') { - throw new Error('Invalid .wdraft file: missing layerCount.'); + let offset = 4; + + // Version + const version = bytes[offset++]; + if (version !== WDRAFT_VERSION) { + throw new Error(`Unsupported .wdraft version: ${version}. Expected ${WDRAFT_VERSION}.`); } - if (!Array.isArray(obj['layers']) || obj['layers'].length === 0) { - throw new Error('Invalid .wdraft file: layers must be a non-empty array.'); + const canvasWidth = view.getUint32(offset, true); + offset += 4; + const canvasHeight = view.getUint32(offset, true); + offset += 4; + const layerCount = view.getUint32(offset, true); + offset += 4; + + const activeLayerIdLen = view.getUint16(offset, true); + offset += 2; + const activeLayerId = dec.decode(bytes.subarray(offset, offset + activeLayerIdLen)); + offset += activeLayerIdLen; + + const layerEntries = view.getUint16(offset, true); + offset += 2; + + if (layerEntries === 0) { + throw new Error('Invalid .wdraft file: no layers.'); } - const layers: WdraftLayer[] = (obj['layers'] as unknown[]).map((item, index) => { - if (typeof item !== 'object' || item === null) { - throw new Error(`Invalid .wdraft file: layer ${index} is not an object.`); - } - const layer = item as Record; - if ( - typeof layer['id'] !== 'string' || - typeof layer['name'] !== 'string' || - typeof layer['visible'] !== 'boolean' || - typeof layer['width'] !== 'number' || - typeof layer['height'] !== 'number' || - typeof layer['imageDataUrl'] !== 'string' - ) { - throw new Error(`Invalid .wdraft file: layer ${index} is missing required fields.`); - } - return { - id: layer['id'], - name: layer['name'], - visible: layer['visible'], - width: layer['width'], - height: layer['height'], - imageDataUrl: layer['imageDataUrl'], - }; - }); + const layers: WdraftLayerMeta[] = []; + const pngBuffers: Uint8Array[] = []; + + for (let i = 0; i < layerEntries; i++) { + const idLen = view.getUint16(offset, true); + offset += 2; + const id = dec.decode(bytes.subarray(offset, offset + idLen)); + offset += idLen; + + const nameLen = view.getUint16(offset, true); + offset += 2; + const name = dec.decode(bytes.subarray(offset, offset + nameLen)); + offset += nameLen; + + const visible = bytes[offset++] !== 0; + + const width = view.getUint32(offset, true); + offset += 4; + const height = view.getUint32(offset, true); + offset += 4; + + const pngLen = view.getUint32(offset, true); + offset += 4; + const png = bytes.slice(offset, offset + pngLen); + offset += pngLen; + + layers.push({id, name, visible, width, height}); + pngBuffers.push(png); + } return { - version: obj['version'], - canvasWidth: obj['canvasWidth'], - canvasHeight: obj['canvasHeight'], - activeLayerId: obj['activeLayerId'], - layerCount: obj['layerCount'], - layers, + meta: {version, canvasWidth, canvasHeight, activeLayerId, layerCount, layers}, + pngBuffers, }; } diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 670c430..e1013a0 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,5 +1,10 @@ import type {LayerDocumentSnapshot, LayerSummary} from './layer-manager'; -import {WDRAFT_MIME_TYPE, WDRAFT_VERSION, type WdraftFile, type WdraftLayer} from './project-file'; +import { + serializeWdraftBinary, + WDRAFT_VERSION, + type ParsedWdraftFile, + type WdraftFileMeta, +} from './project-file'; import { drawLine, drawPoint, @@ -306,48 +311,56 @@ export class WebDraftEditor extends EventTarget { async exportProject(): Promise { const snapshot = this.layerManager.captureDocument(); - const layers: WdraftLayer[] = snapshot.layers.map((layer) => { - const canvas = document.createElement('canvas'); - canvas.width = layer.imageData.width; - canvas.height = layer.imageData.height; - canvas.getContext('2d')!.putImageData(layer.imageData, 0, 0); - return { - id: layer.layerId, - name: layer.name, - visible: layer.visible, - width: layer.imageData.width, - height: layer.imageData.height, - imageDataUrl: canvas.toDataURL('image/png'), - }; - }); - const file: WdraftFile = { + const pngBuffers = await Promise.all( + snapshot.layers.map(async (layer) => { + const canvas = document.createElement('canvas'); + canvas.width = layer.imageData.width; + canvas.height = layer.imageData.height; + canvas.getContext('2d')!.putImageData(layer.imageData, 0, 0); + const blob = await new Promise((resolve, reject) => + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), + 'image/png', + ), + ); + return new Uint8Array(await blob.arrayBuffer()); + }), + ); + const meta: WdraftFileMeta = { version: WDRAFT_VERSION, canvasWidth: this.state.canvasWidth, canvasHeight: this.state.canvasHeight, activeLayerId: snapshot.activeLayerId, layerCount: snapshot.layerCount, - layers, + layers: snapshot.layers.map((l) => ({ + id: l.layerId, + name: l.name, + visible: l.visible, + width: l.imageData.width, + height: l.imageData.height, + })), }; - return new Blob([JSON.stringify(file)], {type: WDRAFT_MIME_TYPE}); + return serializeWdraftBinary(meta, pngBuffers); } - async importProject(file: WdraftFile): Promise { + async importProject(parsed: ParsedWdraftFile): Promise { + const {meta, pngBuffers} = parsed; const before = this.captureHistorySnapshot(); const imageDataList = await Promise.all( - file.layers.map((l) => loadDataUrlAsImageData(l.imageDataUrl, l.width, l.height)), + meta.layers.map((l, i) => loadPngBytesAsImageData(pngBuffers[i], l.width, l.height)), ); const newDocument = { - activeLayerId: file.activeLayerId, - layerCount: file.layerCount, - layers: file.layers.map((l, i) => ({ + activeLayerId: meta.activeLayerId, + layerCount: meta.layerCount, + layers: meta.layers.map((l, i) => ({ layerId: l.id, name: l.name, visible: l.visible, imageData: imageDataList[i], })), }; - this.state.canvasWidth = file.canvasWidth; - this.state.canvasHeight = file.canvasHeight; + this.state.canvasWidth = meta.canvasWidth; + this.state.canvasHeight = meta.canvasHeight; this.layerManager.restoreDocument(newDocument); this.applyCanvasSize(); this.clearSelection(); @@ -1013,10 +1026,12 @@ function createImageData(buffer: {data: Uint8ClampedArray; width: number; height return new ImageData(data, buffer.width, buffer.height); } -function loadDataUrlAsImageData(dataUrl: string, width: number, height: number): Promise { +function loadPngBytesAsImageData(bytes: Uint8Array, width: number, height: number): Promise { return new Promise((resolve, reject) => { + const url = URL.createObjectURL(new Blob([(bytes.buffer as ArrayBuffer).slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], {type: 'image/png'})); const img = new Image(); img.onload = () => { + URL.revokeObjectURL(url); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -1024,7 +1039,10 @@ function loadDataUrlAsImageData(dataUrl: string, width: number, height: number): ctx.drawImage(img, 0, 0); resolve(ctx.getImageData(0, 0, width, height)); }; - img.onerror = () => reject(new Error('Failed to load layer image.')); - img.src = dataUrl; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Failed to load layer image.')); + }; + img.src = url; }); } diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 2488fac..cd41c38 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -1,4 +1,4 @@ -import {parseWdraftFile} from '../core/project-file'; +import {parseWdraftBinary} from '../core/project-file'; import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {openCameraPanel} from './camera-panel'; @@ -457,7 +457,7 @@ export function createProjectSection(editor: WebDraftEditor, status: StatusRepor reader.onload = () => { void (async () => { try { - const parsed = parseWdraftFile(reader.result as string); + const parsed = parseWdraftBinary(reader.result as ArrayBuffer); await editor.importProject(parsed); status.show('Project opened.', 'success'); } catch (error) { @@ -467,7 +467,7 @@ export function createProjectSection(editor: WebDraftEditor, status: StatusRepor } })(); }; - reader.readAsText(file); + reader.readAsArrayBuffer(file); }); const openButton = createCommandButton('Open project', () => wdraftInput.click()); diff --git a/docs/wdraft-format.md b/docs/wdraft-format.md index 3a9fa12..8386936 100644 --- a/docs/wdraft-format.md +++ b/docs/wdraft-format.md @@ -1,4 +1,4 @@ -# `.wdraft` File Format +# `.wdraft` File Format (v2) ## Overview @@ -6,79 +6,77 @@ - **Extension:** `.wdraft` - **MIME type:** `application/x-webdraft` -- **Encoding:** UTF-8 JSON +- **Encoding:** binary (little-endian) --- -## Schema - -```json -{ - "version": 1, - "canvasWidth": 900, - "canvasHeight": 620, - "activeLayerId": "layer-3", - "layerCount": 5, - "layers": [ - { - "id": "layer-1", - "name": "Background", - "visible": true, - "width": 900, - "height": 620, - "imageDataUrl": "data:image/png;base64,..." - } - ] -} -``` - -### Top-level fields - -| Field | Type | Description | -|---|---|---| -| `version` | `number` | Format version. Currently `1`. | -| `canvasWidth` | `number` | Canvas width in pixels. | -| `canvasHeight` | `number` | Canvas height in pixels. | -| `activeLayerId` | `string` | ID of the layer that was active at save time. | -| `layerCount` | `number` | Internal counter used to generate unique layer IDs after import. Must be ≥ the highest layer index. | -| `layers` | `WdraftLayer[]` | Ordered array of layers, bottom to top. | - -### Layer fields (`WdraftLayer`) - -| Field | Type | Description | -|---|---|---| -| `id` | `string` | Unique layer identifier (e.g. `"layer-1"`). | -| `name` | `string` | Display name shown in the layers panel. | -| `visible` | `boolean` | Whether the layer is visible when compositing. | -| `width` | `number` | Width of the layer canvas in pixels. | -| `height` | `number` | Height of the layer canvas in pixels. | -| `imageDataUrl` | `string` | PNG encoded as a base64 data URL (`data:image/png;base64,...`). | +## Binary layout + +All multi-byte integers are **unsigned, little-endian**. + +### Header + +| Offset | Size | Type | Field | +|--------|------|--------|---------------------------| +| 0 | 4 | bytes | Magic: `0x57 0x44 0x46 0x54` (`"WDFT"`) | +| 4 | 1 | uint8 | Version (currently `2`) | +| 5 | 4 | uint32 | Canvas width (px) | +| 9 | 4 | uint32 | Canvas height (px) | +| 13 | 4 | uint32 | Layer count (internal counter) | +| 17 | 2 | uint16 | Active layer ID length (bytes) | +| 19 | N | UTF-8 | Active layer ID | +| 19+N | 2 | uint16 | Number of layer entries | + +### Per-layer block (repeated) + +| Size | Type | Field | +|------|--------|-------------------------------| +| 2 | uint16 | ID length (bytes) | +| N | UTF-8 | Layer ID | +| 2 | uint16 | Name length (bytes) | +| N | UTF-8 | Layer name | +| 1 | uint8 | Visible (`0` = hidden, `1` = visible) | +| 4 | uint32 | Layer canvas width (px) | +| 4 | uint32 | Layer canvas height (px) | +| 4 | uint32 | PNG data length (bytes) | +| N | bytes | PNG data (raw, no base64) | + +Layers are stored bottom-to-top (index 0 = bottommost layer). + +--- + +## Fields + +- **Layer count** — internal counter used to generate unique layer IDs after import; must be ≥ the highest layer index. +- **Layer canvas width/height** — actual dimensions of the layer's canvas (may differ from canvas width/height if the canvas was resized after the layer was created). +- **PNG data** — lossless PNG encoded directly as raw bytes. No base64 encoding. --- ## File size -Each layer's pixel data is stored as a lossless PNG base64 data URL. Approximate sizes: +Without base64 overhead, files are ~25–33% smaller than a JSON equivalent. | Canvas size | Layers | Estimated file size | -|---|---|---| -| 900 × 620 | 1 | ~300 KB | -| 900 × 620 | 5 | ~1.5 MB | -| 1920 × 1080 | 5 | ~5–10 MB | +|-------------|--------|---------------------| +| 900 × 620 | 1 | ~200 KB | +| 900 × 620 | 5 | ~1 MB | +| 1920 × 1080 | 5 | ~4–7 MB | Mostly-transparent layers compress very well; fully painted layers are larger. --- -## Versioning and compatibility +## Versioning -- The `version` field is checked on import. Files with a version other than `1` are rejected with an error. -- Future versions will increment this number and may add new fields. Parsers should reject unknown versions rather than silently ignore unrecognised fields. +- The version byte is checked on import. Files with a version other than `2` are rejected. +- The magic bytes `WDFT` identify the file format independently of version. +- Version 1 was a JSON/base64 format — those files are not supported by this parser. --- ## Usage in WebDraft -**Save project** — serialises all layers to a `.wdraft` file and downloads it. +**Save project** — serialises all layers to a `.wdraft` binary file and downloads it. -**Open project** — reads a `.wdraft` file, restores canvas size and all layers, and pushes a history entry so the action can be undone. +**Open project** — reads a `.wdraft` file with `FileReader.readAsArrayBuffer`, parses the binary, restores canvas size and all layers, and pushes a history entry so the action can be undone. From 198a82eec6ff52e6ecfa511a36082de38eae9215 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 10:37:37 +0200 Subject: [PATCH 35/62] Add interactive color picker with multiple tabs (HSV, wheel, HS rectangle, CMYK), draggable UI, palette support, and live updates. --- app-v3/src/styles.css | 452 ++++++++++++++- app-v3/src/ui/color-picker.ts | 1008 ++++++++++++++++++++++++++++++++- 2 files changed, 1438 insertions(+), 22 deletions(-) diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index ed4ccb3..a49d482 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -235,14 +235,7 @@ select { grid-template-columns: 24px 1fr; align-items: center; cursor: pointer; -} - -.color-picker input { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; + user-select: none; } .color-picker__swatch { @@ -250,6 +243,447 @@ select { height: 24px; border: 1px solid #59606d; border-radius: 50%; + flex-shrink: 0; +} + +/* ---- Advanced color picker popup ---- */ + +.cp-backdrop { + position: fixed; + inset: 0; + z-index: 999; + background: rgba(0, 0, 0, 0.45); +} + +.cp-popup { + position: fixed; + z-index: 1000; + background: #1e2329; + border: 1px solid #3a4049; + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + box-sizing: border-box; + user-select: none; +} + +/* Drag handle */ +.cp-drag-handle { + display: flex; + justify-content: center; + align-items: center; + height: 16px; + cursor: grab; + margin: -4px -4px 0; + border-radius: 5px 5px 0 0; + color: #59606d; + transition: color 0.15s; +} + +.cp-drag-handle:hover { + color: #c9d1dc; +} + +.cp-drag-handle:active { + cursor: grabbing; +} + +.cp-drag-grip { + font-size: 16px; + line-height: 1; + letter-spacing: 2px; + pointer-events: none; +} + +/* Mobile: centered modal */ +.cp-popup--mobile { + left: 50% !important; + top: 50% !important; + transform: translate(-50%, -50%); + max-width: min(520px, 96vw); + max-height: 90vh; + overflow-y: auto; +} + +.cp-popup--mobile .cp-drag-handle { + display: none; +} + +@media (max-width: 560px) { + .cp-popup--mobile .cp-middle { + flex-direction: column; + } + + .cp-popup--mobile .cp-right { + width: 100%; + } + + .cp-popup--mobile .cp-canvas { + width: min(220px, 70vw); + height: min(220px, 70vw); + } + + .cp-popup--mobile .cp-hue-bar { + height: min(220px, 70vw); + } + + .cp-popup--mobile .cp-wheel { + width: min(250px, 80vw); + height: min(250px, 80vw); + } +} + +/* Top bar */ +.cp-topbar { + display: flex; + align-items: center; + gap: 6px; +} + +.cp-btn-group { + display: flex; + gap: 2px; +} + +.cp-toggle-btn { + background: #252b33; + border: 1px solid #3a4049; + border-radius: 3px; + color: #7a8394; + font-size: 11px; + padding: 2px 7px; + cursor: pointer; +} + +.cp-toggle-btn.is-active { + background: #2d3a4a; + border-color: #5b9cff; + color: #c9d1dc; +} + +/* Middle: left + right */ +.cp-middle { + display: flex; + gap: 10px; +} + +/* Left: tabs */ +.cp-left { + display: flex; + flex-direction: column; + gap: 6px; +} + +.cp-tabs { + display: flex; + gap: 2px; +} + +.cp-tab-btn { + width: 30px; + height: 26px; + background: #252b33; + border: 1px solid #3a4049; + border-radius: 3px; + color: #7a8394; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.cp-tab-btn svg { + width: 16px; + height: 16px; + pointer-events: none; +} + +.cp-tab-btn.is-active { + background: #2d3a4a; + border-color: #5b9cff; + color: #c9d1dc; +} + +.cp-tab-content { + flex: 1; +} + +/* Canvas-based pickers */ +.cp-hsv-wrap { + display: flex; + gap: 6px; + align-items: flex-start; +} + +.cp-canvas { + display: block; + width: 220px; + height: 220px; + flex-shrink: 0; + border-radius: 2px; + cursor: crosshair; + touch-action: none; +} + +.cp-hue-bar { + display: block; + width: 14px; + height: 220px; + flex-shrink: 0; + border-radius: 2px; + cursor: ns-resize; + touch-action: none; +} + +.cp-wheel { + display: block; + width: 250px; + height: 250px; + border-radius: 50%; + cursor: crosshair; + touch-action: none; +} + +/* CMYK tab */ +.cp-cmyk-wrap { + display: flex; + flex-direction: column; + gap: 10px; + padding-top: 4px; + width: 240px; +} + +.cp-cmyk-row { + display: flex; + align-items: center; + gap: 6px; +} + +.cp-cmyk-label { + color: #7a8394; + font-size: 12px; + font-weight: 600; + width: 14px; + text-align: center; + flex-shrink: 0; +} + +.cp-cmyk-track { + flex: 1; + min-width: 0; + accent-color: #5b9cff; + cursor: pointer; +} + +.cp-cmyk-val { + width: 38px !important; + flex: none !important; + text-align: center; +} + +/* Right panel */ +.cp-right { + display: flex; + flex-direction: column; + gap: 5px; + width: 200px; + flex-shrink: 0; +} + +.cp-ch-row { + display: flex; + align-items: center; + gap: 5px; +} + +.cp-ch-label { + color: #7a8394; + font-size: 11px; + font-weight: 600; + width: 12px; + text-align: right; + flex-shrink: 0; +} + +.cp-ch-slider { + flex: 1; + min-width: 0; + height: 14px; + -webkit-appearance: none; + appearance: none; + border-radius: 3px; + background: linear-gradient(to right, var(--grad-from, #000), var(--grad-to, #fff)); + outline: none; + cursor: pointer; +} + +.cp-ch-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 10px; + height: 14px; + background: #fff; + border: 1px solid rgba(0,0,0,0.5); + border-radius: 2px; + cursor: pointer; +} + +.cp-ch-slider::-moz-range-thumb { + width: 10px; + height: 14px; + background: #fff; + border: 1px solid rgba(0,0,0,0.5); + border-radius: 2px; + cursor: pointer; +} + +.cp-ch-val { + color: #c9d1dc; + font-size: 11px; + width: 28px; + text-align: right; + flex-shrink: 0; +} + +.cp-icon-btn { + width: 18px; + height: 18px; + background: #252b33; + border: 1px solid #3a4049; + border-radius: 3px; + color: #c9d1dc; + font-size: 13px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.cp-hex-row { + display: flex; + align-items: center; + gap: 5px; + margin-top: 2px; +} + +.cp-field-input { + flex: 1; + min-width: 0; + background: #12161b; + border: 1px solid #3a4049; + border-radius: 3px; + color: #c9d1dc; + font-size: 12px; + padding: 3px 5px; + outline: none; + box-sizing: border-box; +} + +.cp-field-input:focus { + border-color: #5b9cff; +} + +.cp-hex-input { + font-family: monospace; +} + +input[type="number"].cp-field-input { + -moz-appearance: textfield; +} + +input[type="number"].cp-field-input::-webkit-outer-spin-button, +input[type="number"].cp-field-input::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +/* Palette */ +.cp-palette { + display: flex; + align-items: flex-start; + gap: 4px; + flex-wrap: nowrap; + margin-top: 2px; +} + +.cp-palette-add { + width: 18px; + height: 18px; + background: #252b33; + border: 1px solid #3a4049; + border-radius: 3px; + color: #c9d1dc; + font-size: 14px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.cp-palette-grid { + display: flex; + flex-wrap: wrap; + gap: 3px; + flex: 1; +} + +.cp-palette-swatch { + width: 16px; + height: 16px; + border: 1px solid #59606d; + border-radius: 2px; + cursor: pointer; + padding: 0; + flex-shrink: 0; +} + +.cp-palette-swatch:hover { + border-color: #c9d1dc; +} + +/* Bottom bar: current / previous */ +.cp-bottom-bar { + display: flex; + flex-direction: column; + gap: 3px; + border-top: 1px solid #2d3340; + padding-top: 6px; +} + +.cp-color-row { + display: flex; + align-items: center; + gap: 8px; +} + +.cp-color-row-label { + color: #7a8394; + font-size: 11px; + width: 56px; + flex-shrink: 0; +} + +.cp-color-strip { + flex: 1; + height: 18px; + border: 1px solid #3a4049; + border-radius: 3px; +} + +.cp-color-strip--old { + cursor: pointer; +} + +.cp-color-strip--old:hover { + border-color: #c9d1dc; } .range-control { @@ -272,7 +706,7 @@ select { .command-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } diff --git a/app-v3/src/ui/color-picker.ts b/app-v3/src/ui/color-picker.ts index a318fbd..b2a8734 100644 --- a/app-v3/src/ui/color-picker.ts +++ b/app-v3/src/ui/color-picker.ts @@ -8,31 +8,1013 @@ export type ColorPickerControl = HTMLElement & { setValue: (color: string) => void; }; +// Canvas sizes +const SB = 220; // SB square / HS rect +const HUE_BAR = 14; +const WHEEL = 250; +const WHEEL_OUTER = 118; +const WHEEL_INNER = 88; + +const PALETTE_KEY = 'webdraft-color-palette'; + export function createColorPicker(options: ColorPickerOptions): ColorPickerControl { - const wrapper = document.createElement('label'); + let hsv = hexToHsv(options.value); + let currentHex = normalizeHex(options.value); + let oldColor = currentHex; + let activeTab = 0; + let rangeMax = 255; // 0..100 or 0..255 + let popup: HTMLElement | null = null; + let backdrop: HTMLElement | null = null; + + // --- Swatch row --- + const wrapper = document.createElement('div'); wrapper.className = 'color-picker'; const swatch = document.createElement('span'); swatch.className = 'color-picker__swatch'; - swatch.style.backgroundColor = options.value; + swatch.style.backgroundColor = currentHex; - const text = document.createElement('span'); - text.textContent = options.label; + const labelEl = document.createElement('span'); + labelEl.textContent = options.label; - const input = document.createElement('input'); - input.type = 'color'; - input.value = options.value; - input.addEventListener('input', () => { - swatch.style.backgroundColor = input.value; - options.onChange(input.value); + wrapper.append(swatch, labelEl); + wrapper.addEventListener('pointerdown', (e) => { + e.stopPropagation(); + if (popup) { closePopup(); } else { openPopup(); } }); - wrapper.append(swatch, text, input); + // ------------------------------------------------------------------ open/close + + function isTouchDevice(): boolean { + return window.matchMedia('(pointer: coarse)').matches; + } + + function openPopup() { + oldColor = currentHex; + + popup = document.createElement('div'); + popup.className = 'cp-popup'; + + const handle = document.createElement('div'); + handle.className = 'cp-drag-handle'; + handle.innerHTML = ``; + + const top = buildTopBar(); + const middle = document.createElement('div'); + middle.className = 'cp-middle'; + + const left = document.createElement('div'); + left.className = 'cp-left'; + left.append(buildTabIcons(), buildTabContent()); + + const right = buildRightPanel(); + middle.append(left, right); + + const bottom = buildBottom(); + + popup.append(handle, top, middle, bottom); + + if (isTouchDevice()) { + backdrop = document.createElement('div'); + backdrop.className = 'cp-backdrop'; + backdrop.addEventListener('pointerdown', () => closePopup()); + document.body.append(backdrop); + popup.classList.add('cp-popup--mobile'); + } else { + makeDraggable(handle, popup); + } + + document.body.append(popup); + positionPopup(); + switchTab(activeTab, left.querySelector('.cp-tab-content')!); + + document.addEventListener('pointerdown', onOutsideClick); + document.addEventListener('keydown', onEscape); + } + + function closePopup() { + popup?.remove(); + backdrop?.remove(); + popup = null; + backdrop = null; + document.removeEventListener('pointerdown', onOutsideClick); + document.removeEventListener('keydown', onEscape); + } + + function onOutsideClick(e: PointerEvent) { + if (popup && !popup.contains(e.target as Node) && !wrapper.contains(e.target as Node) && e.target !== backdrop) { + closePopup(); + } + } + + function onEscape(e: KeyboardEvent) { + if (e.key === 'Escape') closePopup(); + } + + function positionPopup() { + if (!popup) return; + if (isTouchDevice()) return; // mobile is centered via CSS + const rect = wrapper.getBoundingClientRect(); + const pw = popup.offsetWidth || 510; + const ph = popup.offsetHeight || 340; + let left = rect.right + 8; + let top = rect.top; + if (left + pw > window.innerWidth) left = rect.left - pw - 8; + if (left < 8) left = 8; + if (top + ph > window.innerHeight) top = window.innerHeight - ph - 8; + if (top < 8) top = 8; + popup.style.left = `${left}px`; + popup.style.top = `${top}px`; + } + + function makeDraggable(handle: HTMLElement, target: HTMLElement) { + let dragging = false; + let startX = 0, startY = 0, startLeft = 0, startTop = 0; + + handle.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + dragging = true; + handle.setPointerCapture(e.pointerId); + startX = e.clientX; + startY = e.clientY; + startLeft = parseInt(target.style.left, 10) || 0; + startTop = parseInt(target.style.top, 10) || 0; + e.stopPropagation(); + }); + + handle.addEventListener('pointermove', (e) => { + if (!dragging) return; + const newLeft = Math.max(0, Math.min(window.innerWidth - target.offsetWidth, startLeft + e.clientX - startX)); + const newTop = Math.max(0, Math.min(window.innerHeight - target.offsetHeight, startTop + e.clientY - startY)); + target.style.left = `${newLeft}px`; + target.style.top = `${newTop}px`; + }); + + handle.addEventListener('pointerup', () => { dragging = false; }); + } + + // ------------------------------------------------------------------ top bar + + function buildTopBar(): HTMLElement { + const bar = document.createElement('div'); + bar.className = 'cp-topbar'; + + const rangeGroup = document.createElement('div'); + rangeGroup.className = 'cp-btn-group'; + const btn100 = makeToggleBtn('0..100', rangeMax === 100, () => { rangeMax = 100; syncRangeButtons(); syncRightPanel(); }); + const btn255 = makeToggleBtn('0..255', rangeMax === 255, () => { rangeMax = 255; syncRangeButtons(); syncRightPanel(); }); + + rangeGroup.append(btn100, btn255); + bar.append(rangeGroup); + + function syncRangeButtons() { + btn100.classList.toggle('is-active', rangeMax === 100); + btn255.classList.toggle('is-active', rangeMax === 255); + } + + return bar; + } + + // ------------------------------------------------------------------ tab icons + + const TAB_ICONS = [ + // HSV square + ` + + + `, + // Color wheel + ` + + + `, + // HS rectangle + ` + + + `, + // CMYK sliders + ` + + + + `, + ]; + + function buildTabIcons(): HTMLElement { + const bar = document.createElement('div'); + bar.className = 'cp-tabs'; + TAB_ICONS.forEach((svg, i) => { + const btn = document.createElement('button'); + btn.className = 'cp-tab-btn' + (i === activeTab ? ' is-active' : ''); + btn.innerHTML = svg; + btn.addEventListener('pointerdown', (e) => { e.stopPropagation(); }); + btn.addEventListener('click', () => { + activeTab = i; + bar.querySelectorAll('.cp-tab-btn').forEach((b, j) => b.classList.toggle('is-active', j === i)); + const content = popup!.querySelector('.cp-tab-content') as HTMLElement; + switchTab(i, content); + }); + bar.append(btn); + }); + return bar; + } + + function buildTabContent(): HTMLElement { + const content = document.createElement('div'); + content.className = 'cp-tab-content'; + return content; + } + + function switchTab(tab: number, content: HTMLElement) { + content.innerHTML = ''; + if (tab === 0) content.append(buildHsvTab()); + else if (tab === 1) content.append(buildWheelTab()); + else if (tab === 2) content.append(buildHsRectTab()); + else content.append(buildCmykTab()); + } + + // ------------------------------------------------------------------ Tab 1: HSV square + bar + + function buildHsvTab(): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = 'cp-hsv-wrap'; + + const sbCanvas = document.createElement('canvas'); + sbCanvas.className = 'cp-canvas'; + sbCanvas.width = SB; + sbCanvas.height = SB; + + const hueCanvas = document.createElement('canvas'); + hueCanvas.className = 'cp-hue-bar'; + hueCanvas.width = HUE_BAR; + hueCanvas.height = SB; + + wrap.append(sbCanvas, hueCanvas); + + drawSb(sbCanvas); + drawHueBar(hueCanvas); + bindSbDrag(sbCanvas); + bindHueBarDrag(hueCanvas, sbCanvas); + + return wrap; + } + + function drawSb(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext('2d')!; + const W = canvas.width, H = canvas.height; + ctx.fillStyle = `hsl(${hsv.h}, 100%, 50%)`; + ctx.fillRect(0, 0, W, H); + const wg = ctx.createLinearGradient(0, 0, W, 0); + wg.addColorStop(0, 'rgba(255,255,255,1)'); + wg.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = wg; + ctx.fillRect(0, 0, W, H); + const bg = ctx.createLinearGradient(0, 0, 0, H); + bg.addColorStop(0, 'rgba(0,0,0,0)'); + bg.addColorStop(1, 'rgba(0,0,0,1)'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, W, H); + const cx = hsv.s * W, cy = (1 - hsv.v) * H; + ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; ctx.lineWidth = 2; ctx.stroke(); + ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); + ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); + } + + function drawHueBar(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext('2d')!; + const W = canvas.width, H = canvas.height; + const g = ctx.createLinearGradient(0, 0, 0, H); + ['#f00','#ff0','#0f0','#0ff','#00f','#f0f','#f00'].forEach((c, i) => g.addColorStop(i / 6, c)); + ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); + const cy = (hsv.h / 360) * H; + ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, cy - 2, W, 4); + ctx.fillStyle = '#fff'; ctx.fillRect(0, cy - 1, W, 2); + } + + function bindSbDrag(canvas: HTMLCanvasElement) { + let down = false; + canvas.addEventListener('pointerdown', (e) => { down = true; canvas.setPointerCapture(e.pointerId); pick(e); }); + canvas.addEventListener('pointermove', (e) => { if (down) pick(e); }); + canvas.addEventListener('pointerup', () => { down = false; }); + function pick(e: PointerEvent) { + const r = canvas.getBoundingClientRect(); + const s = clamp01((e.clientX - r.left) / SB); + const v = clamp01(1 - (e.clientY - r.top) / SB); + hsv = {h: hsv.h, s, v}; + applyHsv(); + drawSb(canvas); + } + } + + function bindHueBarDrag(hueCanvas: HTMLCanvasElement, sbCanvas: HTMLCanvasElement) { + let down = false; + hueCanvas.addEventListener('pointerdown', (e) => { down = true; hueCanvas.setPointerCapture(e.pointerId); pick(e); }); + hueCanvas.addEventListener('pointermove', (e) => { if (down) pick(e); }); + hueCanvas.addEventListener('pointerup', () => { down = false; }); + function pick(e: PointerEvent) { + const r = hueCanvas.getBoundingClientRect(); + hsv = {h: clamp(((e.clientY - r.top) / SB) * 360, 0, 360), s: hsv.s, v: hsv.v}; + applyHsv(); + drawSb(sbCanvas); + drawHueBar(hueCanvas); + } + } + + // ------------------------------------------------------------------ Tab 2: Color wheel + + function buildWheelTab(): HTMLElement { + const canvas = document.createElement('canvas'); + canvas.className = 'cp-wheel'; + canvas.width = WHEEL; + canvas.height = WHEEL; + drawWheel(canvas); + bindWheelDrag(canvas); + return canvas; + } + + function drawWheel(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext('2d')!; + const cx = WHEEL / 2, cy = WHEEL / 2; + ctx.clearRect(0, 0, WHEEL, WHEEL); + + // 1. Triangle first (via offscreen canvas so transparent pixels don't overwrite) + const tri = triangleVertices(cx, cy, hsv.h); + drawTriangle(ctx, tri); + + // 2. Hue ring on top, clipped to annulus (no destination-out needed) + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, WHEEL_OUTER, 0, Math.PI * 2, false); + ctx.arc(cx, cy, WHEEL_INNER, 0, Math.PI * 2, true); + ctx.clip('evenodd'); + for (let a = 0; a < 360; a++) { + const s1 = (a - 0.5) * Math.PI / 180; + const s2 = (a + 1.5) * Math.PI / 180; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, WHEEL_OUTER, s1, s2); + ctx.closePath(); + ctx.fillStyle = `hsl(${a}, 100%, 50%)`; + ctx.fill(); + } + ctx.restore(); + + // Ring cursor + const ra = (hsv.h - 90) * Math.PI / 180; + const rm = (WHEEL_OUTER + WHEEL_INNER) / 2; + const rx = cx + rm * Math.cos(ra), ry = cy + rm * Math.sin(ra); + ctx.beginPath(); ctx.arc(rx, ry, 6, 0, Math.PI * 2); + ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); + ctx.beginPath(); ctx.arc(rx, ry, 6, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.stroke(); + + // Triangle cursor + const {vHue, vBlack, vWhite} = tri; + const px = hsv.v * (hsv.s * vHue.x + (1 - hsv.s) * vWhite.x) + (1 - hsv.v) * vBlack.x; + const py = hsv.v * (hsv.s * vHue.y + (1 - hsv.s) * vWhite.y) + (1 - hsv.v) * vBlack.y; + ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; ctx.lineWidth = 2; ctx.stroke(); + ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI * 2); + ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); + } + + type Vec2 = {x: number; y: number}; + type TriVerts = {vHue: Vec2; vBlack: Vec2; vWhite: Vec2}; + + function triangleVertices(cx: number, cy: number, hue: number): TriVerts { + const r = WHEEL_INNER - 4; + const angle = (hue - 90) * Math.PI / 180; + const vHue = {x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle)}; + const vBlack= {x: cx + r * Math.cos(angle + (2 * Math.PI / 3)), y: cy + r * Math.sin(angle + (2 * Math.PI / 3))}; + const vWhite= {x: cx + r * Math.cos(angle - (2 * Math.PI / 3)), y: cy + r * Math.sin(angle - (2 * Math.PI / 3))}; + return {vHue, vBlack, vWhite}; + } + + function drawTriangle(ctx: CanvasRenderingContext2D, {vHue, vBlack, vWhite}: TriVerts) { + const [r, g, b] = hsvToRgb(hsv.h, 1, 1); + const bounds = { + minX: Math.floor(Math.min(vHue.x, vBlack.x, vWhite.x)), + minY: Math.floor(Math.min(vHue.y, vBlack.y, vWhite.y)), + maxX: Math.ceil(Math.max(vHue.x, vBlack.x, vWhite.x)), + maxY: Math.ceil(Math.max(vHue.y, vBlack.y, vWhite.y)), + }; + const W = bounds.maxX - bounds.minX + 1; + const H = bounds.maxY - bounds.minY + 1; + + // Render into offscreen canvas so transparent pixels don't overwrite the main canvas + const offscreen = document.createElement('canvas'); + offscreen.width = W; + offscreen.height = H; + const off = offscreen.getContext('2d')!; + const img = off.createImageData(W, H); + + for (let py = 0; py < H; py++) { + for (let px = 0; px < W; px++) { + const cx2 = bounds.minX + px, cy2 = bounds.minY + py; + const {w0, w1, w2} = bary({x: cx2, y: cy2}, vHue, vBlack, vWhite); + if (w0 < -0.01 || w1 < -0.01 || w2 < -0.01) continue; + const wh = clamp01(w0), wb = clamp01(w1), ww = clamp01(w2); + void wb; + const idx = (py * W + px) * 4; + img.data[idx] = Math.round(wh * r + ww * 255); + img.data[idx+1] = Math.round(wh * g + ww * 255); + img.data[idx+2] = Math.round(wh * b + ww * 255); + img.data[idx+3] = 255; + } + } + off.putImageData(img, 0, 0); + ctx.drawImage(offscreen, bounds.minX, bounds.minY); + } + + function bary(p: Vec2, a: Vec2, b: Vec2, c: Vec2) { + const denom = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y); + const w0 = ((b.y - c.y) * (p.x - c.x) + (c.x - b.x) * (p.y - c.y)) / denom; + const w1 = ((c.y - a.y) * (p.x - c.x) + (a.x - c.x) * (p.y - c.y)) / denom; + return {w0, w1, w2: 1 - w0 - w1}; + } + + function bindWheelDrag(canvas: HTMLCanvasElement) { + let down = false; + let mode: 'ring' | 'tri' | null = null; + const cx = WHEEL / 2, cy = WHEEL / 2; + + canvas.addEventListener('pointerdown', (e) => { + down = true; canvas.setPointerCapture(e.pointerId); + const r = canvas.getBoundingClientRect(); + const px = e.clientX - r.left - cx, py = e.clientY - r.top - cy; + const dist = Math.sqrt(px * px + py * py); + mode = dist >= WHEEL_INNER ? 'ring' : 'tri'; + pick(e); + }); + canvas.addEventListener('pointermove', (e) => { if (down) pick(e); }); + canvas.addEventListener('pointerup', () => { down = false; mode = null; }); + + function pick(e: PointerEvent) { + const r = canvas.getBoundingClientRect(); + const px = e.clientX - r.left, py = e.clientY - r.top; + if (mode === 'ring') { + const angle = Math.atan2(py - cy, px - cx) * 180 / Math.PI + 90; + hsv = {h: (angle + 360) % 360, s: hsv.s, v: hsv.v}; + } else if (mode === 'tri') { + const tri = triangleVertices(cx, cy, hsv.h); + const {w0, w1, w2} = bary({x: px, y: py}, tri.vHue, tri.vBlack, tri.vWhite); + const wh = clamp01(w0), wb = clamp01(w1), ww = clamp01(w2); + const sum = wh + wb + ww; + const nhue = wh / sum, nblack = wb / sum, nwhite = ww / sum; + const v = clamp01(nhue + nwhite); + const s = v > 0 ? clamp01(nhue / v) : 0; + hsv = {h: hsv.h, s, v}; + } + applyHsv(); + drawWheel(canvas); + } + } + + // ------------------------------------------------------------------ Tab 3: HS rect + V bar + + function buildHsRectTab(): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = 'cp-hsv-wrap'; + + const hsCanvas = document.createElement('canvas'); + hsCanvas.className = 'cp-canvas'; + hsCanvas.width = SB; + hsCanvas.height = SB; + + const vCanvas = document.createElement('canvas'); + vCanvas.className = 'cp-hue-bar'; + vCanvas.width = HUE_BAR; + vCanvas.height = SB; + + wrap.append(hsCanvas, vCanvas); + drawHsRect(hsCanvas); + drawVBar(vCanvas); + bindHsRectDrag(hsCanvas); + bindVBarDrag(vCanvas, hsCanvas); + + return wrap; + } + + function drawHsRect(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext('2d')!; + const W = canvas.width, H = canvas.height; + const img = ctx.createImageData(W, H); + + for (let py = 0; py < H; py++) { + for (let px = 0; px < W; px++) { + const h = (px / W) * 360; + const s = 1 - py / H; + const [r, g, b] = hsvToRgb(h, s, hsv.v); + const idx = (py * W + px) * 4; + img.data[idx] = r; img.data[idx+1] = g; img.data[idx+2] = b; img.data[idx+3] = 255; + } + } + ctx.putImageData(img, 0, 0); + + const cx = (hsv.h / 360) * W, cy = (1 - hsv.s) * H; + ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; ctx.lineWidth = 2; ctx.stroke(); + ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); + ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); + } + + function drawVBar(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext('2d')!; + const W = canvas.width, H = canvas.height; + const g = ctx.createLinearGradient(0, 0, 0, H); + g.addColorStop(0, '#fff'); g.addColorStop(1, '#000'); + ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); + const cy = (1 - hsv.v) * H; + ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, cy - 2, W, 4); + ctx.fillStyle = '#fff'; ctx.fillRect(0, cy - 1, W, 2); + } + + function bindHsRectDrag(canvas: HTMLCanvasElement) { + let down = false; + canvas.addEventListener('pointerdown', (e) => { down = true; canvas.setPointerCapture(e.pointerId); pick(e); }); + canvas.addEventListener('pointermove', (e) => { if (down) pick(e); }); + canvas.addEventListener('pointerup', () => { down = false; }); + function pick(e: PointerEvent) { + const r = canvas.getBoundingClientRect(); + const h = clamp(((e.clientX - r.left) / SB) * 360, 0, 360); + const s = clamp01(1 - (e.clientY - r.top) / SB); + hsv = {h, s, v: hsv.v}; + applyHsv(); + drawHsRect(canvas); + } + } + + function bindVBarDrag(vCanvas: HTMLCanvasElement, hsCanvas: HTMLCanvasElement) { + let down = false; + vCanvas.addEventListener('pointerdown', (e) => { down = true; vCanvas.setPointerCapture(e.pointerId); pick(e); }); + vCanvas.addEventListener('pointermove', (e) => { if (down) pick(e); }); + vCanvas.addEventListener('pointerup', () => { down = false; }); + function pick(e: PointerEvent) { + const r = vCanvas.getBoundingClientRect(); + hsv = {h: hsv.h, s: hsv.s, v: clamp01(1 - (e.clientY - r.top) / SB)}; + applyHsv(); + drawHsRect(hsCanvas); + drawVBar(vCanvas); + } + } + + // ------------------------------------------------------------------ Tab 4: CMYK sliders + + function buildCmykTab(): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = 'cp-cmyk-wrap'; + + const [r, g, b] = hsvToRgb(hsv.h, hsv.s, hsv.v); + let {c, m, y, k} = rgbToCmyk(r, g, b); + + const sliders: HTMLElement[] = []; + + function rebuild() { + const [r2, g2, b2] = cmykToRgb(c, m, y, k); + const [h, s, v] = rgbToHsv(r2, g2, b2); + hsv = {h, s, v}; + applyHsv(); + // refresh slider backgrounds + updateCmykBgs(); + } + + function updateCmykBgs() { + const [r2, g2, b2] = hsvToRgb(hsv.h, hsv.s, hsv.v); + const cmyk2 = rgbToCmyk(r2, g2, b2); + c = cmyk2.c; m = cmyk2.m; y = cmyk2.y; k = cmyk2.k; + + const entries: [string, number, (v: number) => void][] = [ + ['C', c, (v) => { c = v; rebuild(); }], + ['M', m, (v) => { m = v; rebuild(); }], + ['Y', y, (v) => { y = v; rebuild(); }], + ['K', k, (v) => { k = v; rebuild(); }], + ]; + sliders.forEach((row, i) => { + const [, val,] = entries[i]; + const inp = row.querySelector('input[type=range]') as HTMLInputElement; + const num = row.querySelector('.cp-cmyk-val') as HTMLInputElement; + if (inp) inp.value = String(Math.round(val)); + if (num) num.value = String(Math.round(val)); + }); + } + + const entries: [string, number, (v: number) => void][] = [ + ['C', c, (v) => { c = v; rebuild(); }], + ['M', m, (v) => { m = v; rebuild(); }], + ['Y', y, (v) => { y = v; rebuild(); }], + ['K', k, (v) => { k = v; rebuild(); }], + ]; + + for (const [label, val, onChange] of entries) { + const row = document.createElement('div'); + row.className = 'cp-cmyk-row'; + + const lbl = document.createElement('span'); + lbl.className = 'cp-cmyk-label'; + lbl.textContent = label; + + const track = document.createElement('input'); + track.type = 'range'; + track.min = '0'; track.max = '100'; track.step = '1'; + track.value = String(Math.round(val)); + track.className = 'cp-cmyk-track'; + + const num = document.createElement('input'); + num.type = 'number'; + num.min = '0'; num.max = '100'; + num.value = String(Math.round(val)); + num.className = 'cp-cmyk-val cp-field-input'; + + const minus = makeIconBtn('−', () => { track.value = String(Math.max(0, +track.value - 1)); onChange(+track.value); num.value = track.value; }); + const plus = makeIconBtn('+', () => { track.value = String(Math.min(100, +track.value + 1)); onChange(+track.value); num.value = track.value; }); + + track.addEventListener('input', () => { onChange(+track.value); num.value = track.value; }); + num.addEventListener('input', () => { const v = clamp(+num.value, 0, 100); onChange(v); track.value = String(v); }); + num.addEventListener('pointerdown', (e) => e.stopPropagation()); + + row.append(lbl, track, num, minus, plus); + sliders.push(row); + wrap.append(row); + } + + return wrap; + } + + // ------------------------------------------------------------------ Right panel + + let rgbSliderEls: {label: string; input: HTMLInputElement; valEl: HTMLElement}[] = []; + let hexInputEl: HTMLInputElement | null = null; + let swatchNewEl: HTMLElement | null = null; + let swatchOldEl: HTMLElement | null = null; + let currentBarEl: HTMLElement | null = null; + + function buildRightPanel(): HTMLElement { + const panel = document.createElement('div'); + panel.className = 'cp-right'; + + // RGB sliders + const channels = ['R', 'G', 'B']; + rgbSliderEls = channels.map((ch) => { + const row = document.createElement('div'); + row.className = 'cp-ch-row'; + const lbl = document.createElement('span'); + lbl.className = 'cp-ch-label'; + lbl.textContent = ch; + const inp = document.createElement('input'); + inp.type = 'range'; + inp.min = '0'; inp.max = '255'; inp.step = '1'; + inp.className = 'cp-ch-slider'; + const val = document.createElement('span'); + val.className = 'cp-ch-val'; + const minus = makeIconBtn('−', () => { inp.value = String(Math.max(0, +inp.value - 1)); onRgbSlider(); }); + const plus = makeIconBtn('+', () => { inp.value = String(Math.min(255, +inp.value + 1)); onRgbSlider(); }); + inp.addEventListener('input', onRgbSlider); + row.append(lbl, inp, val, minus, plus); + panel.append(row); + return {label: ch, input: inp, valEl: val}; + }); + + function onRgbSlider() { + const r = +rgbSliderEls[0].input.value; + const g = +rgbSliderEls[1].input.value; + const b = +rgbSliderEls[2].input.value; + const [h, s, v] = rgbToHsv(r, g, b); + hsv = {h, s, v}; + applyHsvSilentSliders(); + } + + // Hex row + const hexRow = document.createElement('div'); + hexRow.className = 'cp-hex-row'; + const hexLbl = document.createElement('span'); + hexLbl.className = 'cp-ch-label'; + hexLbl.textContent = '#'; + hexInputEl = document.createElement('input'); + hexInputEl.type = 'text'; + hexInputEl.className = 'cp-hex-input cp-field-input'; + hexInputEl.maxLength = 7; + hexInputEl.value = currentHex; + hexInputEl.spellcheck = false; + hexInputEl.addEventListener('input', () => { + const parsed = hexToRgb(hexInputEl!.value); + if (parsed) { + const [h, s, v] = rgbToHsv(...parsed); + hsv = {h, s, v}; + applyHsvSilentHex(); + } + }); + hexInputEl.addEventListener('pointerdown', (e) => e.stopPropagation()); + hexInputEl.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePopup(); }); + hexRow.append(hexLbl, hexInputEl); + panel.append(hexRow); + + // Swatches palette + panel.append(buildPalette()); + + syncRightPanel(); + return panel; + } + + function syncRightPanel() { + if (!popup) return; + const [r, g, b] = hsvToRgb(hsv.h, hsv.s, hsv.v); + rgbSliderEls.forEach(({input, valEl}, i) => { + const val = [r, g, b][i]; + const displayed = rangeMax === 100 ? Math.round((val / 255) * 100) : val; + input.value = String(val); + valEl.textContent = String(displayed); + // Gradient background + const others = [r, g, b].slice(); + const lo = others.map((v2, j) => j === i ? 0 : v2); + const hi = others.map((v2, j) => j === i ? 255 : v2); + input.style.setProperty('--grad-from', `rgb(${lo.join(',')})`); + input.style.setProperty('--grad-to', `rgb(${hi.join(',')})`); + }); + if (hexInputEl) hexInputEl.value = currentHex; + if (swatchNewEl) swatchNewEl.style.backgroundColor = currentHex; + if (currentBarEl) currentBarEl.style.backgroundColor = currentHex; + updateActiveTabCanvas(); + } + + function updateActiveTabCanvas() { + if (!popup) return; + const content = popup.querySelector('.cp-tab-content') as HTMLElement | null; + if (!content) return; + if (activeTab === 0) { + const sb = content.querySelector('.cp-canvas') as HTMLCanvasElement | null; + const hb = content.querySelector('.cp-hue-bar') as HTMLCanvasElement | null; + if (sb) drawSb(sb); + if (hb) drawHueBar(hb); + } else if (activeTab === 1) { + const wh = content.querySelector('.cp-wheel') as HTMLCanvasElement | null; + if (wh) drawWheel(wh); + } else if (activeTab === 2) { + const hs = content.querySelector('.cp-canvas') as HTMLCanvasElement | null; + const vb = content.querySelector('.cp-hue-bar') as HTMLCanvasElement | null; + if (hs) drawHsRect(hs); + if (vb) drawVBar(vb); + } + } + + function buildPalette(): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = 'cp-palette'; + + const addBtn = document.createElement('button'); + addBtn.className = 'cp-palette-add'; + addBtn.textContent = '+'; + addBtn.title = 'Add current color'; + addBtn.addEventListener('click', () => { + const palette = loadPalette(); + if (!palette.includes(currentHex)) { + palette.unshift(currentHex); + if (palette.length > 24) palette.pop(); + savePalette(palette); + } + renderSwatches(); + }); + addBtn.addEventListener('pointerdown', (e) => e.stopPropagation()); + wrap.append(addBtn); + + const grid = document.createElement('div'); + grid.className = 'cp-palette-grid'; + wrap.append(grid); + + function renderSwatches() { + grid.innerHTML = ''; + loadPalette().forEach((hex) => { + const sw = document.createElement('button'); + sw.className = 'cp-palette-swatch'; + sw.style.backgroundColor = hex; + sw.title = hex; + sw.addEventListener('pointerdown', (e) => e.stopPropagation()); + sw.addEventListener('click', () => { + const parsed = hexToRgb(hex); + if (!parsed) return; + const [h, s, v] = rgbToHsv(...parsed); + hsv = {h, s, v}; + currentHex = hex; + applyColor(); + }); + grid.append(sw); + }); + } + renderSwatches(); + return wrap; + } + + // ------------------------------------------------------------------ Bottom bar + + function buildBottom(): HTMLElement { + const bar = document.createElement('div'); + bar.className = 'cp-bottom-bar'; + + const make = (label: string, color: string, isOld: boolean) => { + const row = document.createElement('div'); + row.className = 'cp-color-row'; + const lbl = document.createElement('span'); + lbl.className = 'cp-color-row-label'; + lbl.textContent = label; + const strip = document.createElement('div'); + strip.className = 'cp-color-strip' + (isOld ? ' cp-color-strip--old' : ''); + strip.style.backgroundColor = color; + if (isOld) { + strip.title = 'Click to restore previous color'; + strip.addEventListener('pointerdown', (e) => e.stopPropagation()); + strip.addEventListener('click', () => { + const parsed = hexToRgb(oldColor); + if (!parsed) return; + const [h, s, v] = rgbToHsv(...parsed); + hsv = {h, s, v}; + currentHex = oldColor; + applyColor(); + }); + } else { + currentBarEl = strip; + } + row.append(lbl, strip); + return row; + }; + + bar.append(make('New:', currentHex, false)); + bar.append(make('Previous:', oldColor, true)); + return bar; + } + + // ------------------------------------------------------------------ Apply helpers + + function applyHsv() { + const [r, g, b] = hsvToRgb(hsv.h, hsv.s, hsv.v); + currentHex = rgbToHex(r, g, b); + swatch.style.backgroundColor = currentHex; + options.onChange(currentHex); + syncRightPanel(); + } + + function applyHsvSilentSliders() { + const [r, g, b] = hsvToRgb(hsv.h, hsv.s, hsv.v); + currentHex = rgbToHex(r, g, b); + swatch.style.backgroundColor = currentHex; + options.onChange(currentHex); + if (hexInputEl) hexInputEl.value = currentHex; + if (swatchNewEl) swatchNewEl.style.backgroundColor = currentHex; + if (currentBarEl) currentBarEl.style.backgroundColor = currentHex; + // update slider gradients only + const vals = [r, g, b]; + rgbSliderEls.forEach(({valEl}, i) => { + const displayed = rangeMax === 100 ? Math.round((vals[i] / 255) * 100) : vals[i]; + valEl.textContent = String(displayed); + const lo = vals.map((v2, j) => j === i ? 0 : v2); + const hi = vals.map((v2, j) => j === i ? 255 : v2); + rgbSliderEls[i].input.style.setProperty('--grad-from', `rgb(${lo.join(',')})`); + rgbSliderEls[i].input.style.setProperty('--grad-to', `rgb(${hi.join(',')})`); + }); + updateActiveTabCanvas(); + } + + function applyHsvSilentHex() { + const [r, g, b] = hsvToRgb(hsv.h, hsv.s, hsv.v); + currentHex = rgbToHex(r, g, b); + swatch.style.backgroundColor = currentHex; + options.onChange(currentHex); + // update sliders + canvas but not hex input + const vals = [r, g, b]; + rgbSliderEls.forEach(({input, valEl}, i) => { + input.value = String(vals[i]); + const displayed = rangeMax === 100 ? Math.round((vals[i] / 255) * 100) : vals[i]; + valEl.textContent = String(displayed); + const lo = vals.map((v2, j) => j === i ? 0 : v2); + const hi = vals.map((v2, j) => j === i ? 255 : v2); + input.style.setProperty('--grad-from', `rgb(${lo.join(',')})`); + input.style.setProperty('--grad-to', `rgb(${hi.join(',')})`); + }); + if (swatchNewEl) swatchNewEl.style.backgroundColor = currentHex; + if (currentBarEl) currentBarEl.style.backgroundColor = currentHex; + updateActiveTabCanvas(); + } + + function applyColor() { + swatch.style.backgroundColor = currentHex; + options.onChange(currentHex); + syncRightPanel(); + } + + // ------------------------------------------------------------------ Misc helpers + + function makeToggleBtn(label: string, active: boolean, onClick: () => void): HTMLButtonElement { + const btn = document.createElement('button'); + btn.className = 'cp-toggle-btn' + (active ? ' is-active' : ''); + btn.textContent = label; + btn.addEventListener('pointerdown', (e) => e.stopPropagation()); + btn.addEventListener('click', onClick); + return btn; + } + + function makeIconBtn(label: string, onClick: () => void): HTMLButtonElement { + const btn = document.createElement('button'); + btn.className = 'cp-icon-btn'; + btn.textContent = label; + btn.addEventListener('pointerdown', (e) => e.stopPropagation()); + btn.addEventListener('click', onClick); + return btn; + } return Object.assign(wrapper, { setValue(color: string) { - input.value = color; - swatch.style.backgroundColor = color; + const parsed = hexToRgb(color); + if (!parsed) return; + const [h, s, v] = rgbToHsv(...parsed); + hsv = {h, s, v}; + currentHex = rgbToHex(...parsed); + swatch.style.backgroundColor = currentHex; + if (popup) syncRightPanel(); }, }); } + +// -------------------------------------------------------------------- Palette storage + +function loadPalette(): string[] { + try { return JSON.parse(localStorage.getItem(PALETTE_KEY) ?? '[]'); } catch { return []; } +} + +function savePalette(palette: string[]) { + localStorage.setItem(PALETTE_KEY, JSON.stringify(palette)); +} + +// -------------------------------------------------------------------- Color math + +type Hsv = {h: number; s: number; v: number}; + +function hexToHsv(hex: string): Hsv { + const rgb = hexToRgb(hex); + if (!rgb) return {h: 0, s: 0, v: 1}; + const [h, s, v] = rgbToHsv(...rgb); + return {h, s, v}; +} + +function normalizeHex(hex: string): string { + const rgb = hexToRgb(hex); + return rgb ? rgbToHex(...rgb) : '#000000'; +} + +function hexToRgb(hex: string): [number, number, number] | null { + const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex.trim()); + if (!m) return null; + return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)]; +} + +function rgbToHex(r: number, g: number, b: number): string { + return '#' + [r, g, b].map((n) => Math.round(n).toString(16).padStart(2, '0')).join(''); +} + +function rgbToHsv(r: number, g: number, b: number): [number, number, number] { + const rn = r / 255, gn = g / 255, bn = b / 255; + const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn), delta = max - min; + let h = 0; + if (delta > 0) { + if (max === rn) h = ((gn - bn) / delta) % 6; + else if (max === gn) h = (bn - rn) / delta + 2; + else h = (rn - gn) / delta + 4; + h = (h * 60 + 360) % 360; + } + return [h, max === 0 ? 0 : delta / max, max]; +} + +function hsvToRgb(h: number, s: number, v: number): [number, number, number] { + const c = v * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = v - c; + let r = 0, g = 0, b = 0; + if (h < 60) { r = c; g = x; } + else if (h < 120) { r = x; g = c; } + else if (h < 180) { g = c; b = x; } + else if (h < 240) { g = x; b = c; } + else if (h < 300) { r = x; b = c; } + else { r = c; b = x; } + return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]; +} + +function rgbToCmyk(r: number, g: number, b: number): {c: number; m: number; y: number; k: number} { + const rn = r / 255, gn = g / 255, bn = b / 255; + const k = 1 - Math.max(rn, gn, bn); + if (k === 1) return {c: 0, m: 0, y: 0, k: 100}; + return { + c: Math.round(((1 - rn - k) / (1 - k)) * 100), + m: Math.round(((1 - gn - k) / (1 - k)) * 100), + y: Math.round(((1 - bn - k) / (1 - k)) * 100), + k: Math.round(k * 100), + }; +} + +function cmykToRgb(c: number, m: number, y: number, k: number): [number, number, number] { + const cn = c / 100, mn = m / 100, yn = y / 100, kn = k / 100; + return [ + Math.round(255 * (1 - cn) * (1 - kn)), + Math.round(255 * (1 - mn) * (1 - kn)), + Math.round(255 * (1 - yn) * (1 - kn)), + ]; +} + +function clamp(v: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, v)); } +function clamp01(v: number) { return clamp(v, 0, 1); } From 1940ee2cd4b2070b2ea68cc9c847d54b6bbdca2f Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 10:44:13 +0200 Subject: [PATCH 36/62] Add menu bar with dropdown functionality, refactor app structure into `.app-wrap` container, and improve sidebar and panel height handling --- app-v3/src/main.ts | 8 +- app-v3/src/styles.css | 106 ++++++++++- app-v3/src/ui/menu-bar.ts | 364 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+), 6 deletions(-) create mode 100644 app-v3/src/ui/menu-bar.ts diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index abde705..86619ff 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -1,4 +1,5 @@ import { WebDraftEditor } from './core/webdraft-editor'; +import { createMenuBar } from './ui/menu-bar'; import { createRightPanel } from './ui/right-panel'; import { createStatusToasts } from './ui/status-toasts'; import { createToolbar } from './ui/toolbar'; @@ -10,6 +11,9 @@ if (!app) { throw new Error('Missing #app root element.'); } +const appWrap = document.createElement('div'); +appWrap.className = 'app-wrap'; + const shell = document.createElement('main'); shell.className = 'app-shell'; @@ -29,7 +33,8 @@ const statusToasts = createStatusToasts(); workspace.append(surface); shell.append(sidebar, workspace, rightPanel); -app.append(shell, statusToasts); +appWrap.append(shell); +app.append(appWrap, statusToasts); const editor = new WebDraftEditor(surface, { width: 900, @@ -38,6 +43,7 @@ const editor = new WebDraftEditor(surface, { size: 10 }); +appWrap.prepend(createMenuBar(editor, statusToasts)); sidebar.append(createToolbar(editor, statusToasts)); rightPanel.append(createRightPanel(editor, statusToasts)); editor.mount(); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index a49d482..dcd112b 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -37,19 +37,114 @@ select { border: 0; } +.app-wrap { + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + overflow: hidden; +} + .app-shell { display: grid; grid-template-columns: 240px minmax(0, 1fr) 240px; width: 100%; min-width: 0; - min-height: 100vh; + flex: 1; overflow: hidden; } +/* ── Menu bar ─────────────────────────────────────────── */ + +.menu-bar { + display: flex; + align-items: stretch; + height: 28px; + flex-shrink: 0; + background: #17191d; + border-bottom: 1px solid #343941; + padding: 0 4px; + user-select: none; + z-index: 100; +} + +.menu-btn { + display: flex; + align-items: center; + padding: 0 10px; + height: 100%; + background: none; + border: none; + color: #c8cdd4; + font-size: 13px; + cursor: default; + border-radius: 3px; + white-space: nowrap; +} + +.menu-btn:hover, +.menu-btn--active { + background: #2c3038; + color: #f4f7fb; +} + +.menu-dropdown { + position: fixed; + z-index: 9999; + min-width: 210px; + margin: 0; + padding: 4px 0; + list-style: none; + background: #1e2126; + border: 1px solid #343941; + border-radius: 5px; + box-shadow: 0 8px 24px rgba(0,0,0,.45); +} + +.menu-dropdown__item button { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 5px 14px; + background: none; + border: none; + color: #c8cdd4; + font-size: 13px; + text-align: left; + cursor: default; + gap: 24px; +} + +.menu-dropdown__item button:hover { + background: #2e6fb7; + color: #fff; +} + +.menu-dropdown__item--disabled button { + opacity: .38; + pointer-events: none; +} + +.menu-dropdown__shortcut { + margin-left: auto; + font-size: 11px; + opacity: .55; + white-space: nowrap; +} + +.menu-dropdown__sep { + height: 1px; + margin: 4px 0; + background: #343941; +} + +/* ── Sidebar height adjustment ──────────────────────────── */ + .sidebar { min-width: 0; - min-height: 100vh; - max-height: 100vh; + height: 100%; + max-height: 100%; overflow: auto; border-right: 1px solid #343941; background: #17191d; @@ -57,8 +152,8 @@ select { .right-panel { min-width: 0; - min-height: 100vh; - max-height: 100vh; + height: 100%; + max-height: 100%; overflow: auto; border-left: 1px solid #343941; background: #17191d; @@ -69,6 +164,7 @@ select { place-items: center; width: 100%; min-width: 0; + height: 100%; overflow: auto; padding: 32px; } diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts new file mode 100644 index 0000000..1de0668 --- /dev/null +++ b/app-v3/src/ui/menu-bar.ts @@ -0,0 +1,364 @@ +import { parseWdraftBinary } from '../core/project-file'; +import type { WebDraftEditor } from '../core/webdraft-editor'; +import type { StatusReporter } from './status-toasts'; +import { getErrorMessage } from './status-toasts'; + +type MenuItem = + | { type: 'action'; label: string; action: () => void; shortcut?: string; disabled?: () => boolean } + | { type: 'separator' }; + +type Menu = { label: string; items: MenuItem[] }; + +let openMenu: HTMLElement | null = null; +let openButton: HTMLButtonElement | null = null; + +function closeOpenMenu(): void { + openMenu?.remove(); + openMenu = null; + openButton?.classList.remove('menu-btn--active'); + openButton = null; +} + +function buildDropdown(items: MenuItem[], anchorEl: HTMLElement): HTMLElement { + const dropdown = document.createElement('ul'); + dropdown.className = 'menu-dropdown'; + dropdown.setAttribute('role', 'menu'); + + for (const item of items) { + const li = document.createElement('li'); + li.setAttribute('role', 'none'); + + if (item.type === 'separator') { + li.className = 'menu-dropdown__sep'; + li.setAttribute('role', 'separator'); + } else { + li.className = 'menu-dropdown__item'; + if (item.disabled?.()) li.classList.add('menu-dropdown__item--disabled'); + + const btn = document.createElement('button'); + btn.setAttribute('role', 'menuitem'); + btn.tabIndex = -1; + btn.textContent = item.label; + + if (item.shortcut) { + const kbd = document.createElement('span'); + kbd.className = 'menu-dropdown__shortcut'; + kbd.textContent = item.shortcut; + btn.append(kbd); + } + + btn.addEventListener('click', () => { + if (!item.disabled?.()) { + closeOpenMenu(); + item.action(); + } + }); + li.append(btn); + } + + dropdown.append(li); + } + + const rect = anchorEl.getBoundingClientRect(); + dropdown.style.left = `${rect.left}px`; + dropdown.style.top = `${rect.bottom}px`; + + return dropdown; +} + +function createMenuButton(menu: Menu): HTMLButtonElement { + const btn = document.createElement('button'); + btn.className = 'menu-btn'; + btn.textContent = menu.label; + btn.setAttribute('aria-haspopup', 'true'); + btn.setAttribute('aria-expanded', 'false'); + + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (openMenu && openButton === btn) { + closeOpenMenu(); + return; + } + closeOpenMenu(); + + const dropdown = buildDropdown(menu.items, btn); + document.body.append(dropdown); + openMenu = dropdown; + openButton = btn; + btn.classList.add('menu-btn--active'); + btn.setAttribute('aria-expanded', 'true'); + }); + + btn.addEventListener('mouseenter', () => { + if (openMenu && openButton !== btn) { + closeOpenMenu(); + const dropdown = buildDropdown(menu.items, btn); + document.body.append(dropdown); + openMenu = dropdown; + openButton = btn; + btn.classList.add('menu-btn--active'); + btn.setAttribute('aria-expanded', 'true'); + } + }); + + return btn; +} + +export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): HTMLElement { + const bar = document.createElement('nav'); + bar.className = 'menu-bar'; + bar.setAttribute('aria-label', 'Main menu'); + + const plikInput = document.createElement('input'); + plikInput.type = 'file'; + plikInput.accept = '.wdraft'; + plikInput.className = 'visually-hidden'; + plikInput.addEventListener('change', () => { + const file = plikInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + void (async () => { + try { + const parsed = parseWdraftBinary(reader.result as ArrayBuffer); + await editor.importProject(parsed); + status.show('Projekt otwarty.', 'success'); + } catch (err) { + status.show(getErrorMessage(err), 'error'); + } finally { + plikInput.value = ''; + } + })(); + }; + reader.readAsArrayBuffer(file); + }); + + const imageInput = document.createElement('input'); + imageInput.type = 'file'; + imageInput.accept = 'image/*'; + imageInput.className = 'visually-hidden'; + imageInput.addEventListener('change', () => { + const file = imageInput.files?.[0]; + if (!file) return; + void (async () => { + try { + await editor.importImage(file); + status.show('Obraz zaimportowany.', 'success'); + } catch (err) { + status.show(getErrorMessage(err), 'error'); + } finally { + imageInput.value = ''; + } + })(); + }); + + bar.append(plikInput, imageInput); + + const menus: Menu[] = [ + { + label: 'Plik', + items: [ + { + type: 'action', + label: 'Nowy projekt', + shortcut: 'Ctrl+N', + action: () => { + if (confirm('Nowy projekt? Niezapisane zmiany zostaną utracone.')) { + editor.clear(); + status.show('Nowy projekt.', 'success'); + } + }, + }, + { + type: 'action', + label: 'Otwórz projekt…', + shortcut: 'Ctrl+O', + action: () => plikInput.click(), + }, + { type: 'separator' }, + { + type: 'action', + label: 'Zapisz projekt', + shortcut: 'Ctrl+S', + action: () => { + void (async () => { + try { + const blob = await editor.exportProject(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'projekt.wdraft'; + a.click(); + URL.revokeObjectURL(url); + status.show('Projekt zapisany.', 'success'); + } catch (err) { + status.show(getErrorMessage(err), 'error'); + } + })(); + }, + }, + { type: 'separator' }, + { + type: 'action', + label: 'Importuj obraz…', + action: () => imageInput.click(), + }, + { + type: 'action', + label: 'Eksportuj PNG', + shortcut: 'Ctrl+Shift+E', + action: () => { + void (async () => { + try { + const blob = await editor.exportPng(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'obraz.png'; + a.click(); + URL.revokeObjectURL(url); + status.show('PNG wyeksportowany.', 'success'); + } catch (err) { + status.show(getErrorMessage(err), 'error'); + } + })(); + }, + }, + ], + }, + { + label: 'Edycja', + items: [ + { + type: 'action', + label: 'Cofnij', + shortcut: 'Ctrl+Z', + disabled: () => !editor.canUndo, + action: () => editor.undo(), + }, + { + type: 'action', + label: 'Ponów', + shortcut: 'Ctrl+Y', + disabled: () => !editor.canRedo, + action: () => editor.redo(), + }, + { type: 'separator' }, + { + type: 'action', + label: 'Wytnij', + shortcut: 'Ctrl+X', + action: () => editor.cutSelection(), + }, + { + type: 'action', + label: 'Kopiuj', + shortcut: 'Ctrl+C', + action: () => editor.copySelection(), + }, + { + type: 'action', + label: 'Wklej', + shortcut: 'Ctrl+V', + action: () => editor.pasteSelection(), + }, + { type: 'separator' }, + { + type: 'action', + label: 'Wyczyść warstwę', + action: () => editor.clear(), + }, + ], + }, + { + label: 'Obraz', + items: [ + { + type: 'action', + label: 'Zmień rozmiar płótna…', + action: () => { + const w = prompt('Szerokość (px):', '900'); + const h = prompt('Wysokość (px):', '620'); + const nw = parseInt(w ?? '', 10); + const nh = parseInt(h ?? '', 10); + if (nw > 0 && nh > 0) { + editor.resizeCanvas(nw, nh); + status.show(`Płótno: ${nw}×${nh}`, 'success'); + } + }, + }, + { type: 'separator' }, + { + type: 'action', + label: 'Odwróć kolory', + action: () => editor.invertActiveLayer(), + }, + { + type: 'action', + label: 'Lustro poziome', + action: () => editor.mirrorActiveLayer('horizontal'), + }, + { + type: 'action', + label: 'Lustro pionowe', + action: () => editor.mirrorActiveLayer('vertical'), + }, + { type: 'separator' }, + { + type: 'action', + label: 'Obróć w lewo', + action: () => editor.rotateActiveLayer('left'), + }, + { + type: 'action', + label: 'Obróć w prawo', + action: () => editor.rotateActiveLayer('right'), + }, + ], + }, + { + label: 'Warstwa', + items: [ + { + type: 'action', + label: 'Nowa warstwa', + action: () => { + editor.addLayer(); + status.show('Warstwa dodana.', 'success'); + }, + }, + { + type: 'action', + label: 'Usuń warstwę', + disabled: () => editor.layers.length <= 1, + action: () => { + editor.deleteActiveLayer(); + status.show('Warstwa usunięta.', 'success'); + }, + }, + { type: 'separator' }, + { + type: 'action', + label: 'Przesuń warstwę w górę', + action: () => editor.moveActiveLayerUp(), + }, + { + type: 'action', + label: 'Przesuń warstwę w dół', + action: () => editor.moveActiveLayerDown(), + }, + ], + }, + ]; + + for (const menu of menus) { + bar.append(createMenuButton(menu)); + } + + document.addEventListener('click', closeOpenMenu); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeOpenMenu(); + }); + + return bar; +} From 4beb6a5f76ada60e9a9b16b2faba529209385d84 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 11:09:16 +0200 Subject: [PATCH 37/62] Add i18n support with English and Polish translations, integrate dynamic text into the UI, and refactor static labels to use localization strings --- app-v3/src/core/i18n.ts | 14 +++ app-v3/src/i18n/en.ts | 140 ++++++++++++++++++++++++++++++ app-v3/src/i18n/pl.ts | 140 ++++++++++++++++++++++++++++++ app-v3/src/i18n/types.ts | 138 +++++++++++++++++++++++++++++ app-v3/src/ui/camera-panel.ts | 33 +++---- app-v3/src/ui/color-picker.ts | 14 +-- app-v3/src/ui/layers-panel.ts | 17 ++-- app-v3/src/ui/menu-bar.ts | 97 +++++++++++---------- app-v3/src/ui/status-toasts.ts | 4 +- app-v3/src/ui/toolbar-sections.ts | 97 +++++++++++---------- app-v3/src/ui/toolbar.ts | 7 +- 11 files changed, 571 insertions(+), 130 deletions(-) create mode 100644 app-v3/src/core/i18n.ts create mode 100644 app-v3/src/i18n/en.ts create mode 100644 app-v3/src/i18n/pl.ts create mode 100644 app-v3/src/i18n/types.ts diff --git a/app-v3/src/core/i18n.ts b/app-v3/src/core/i18n.ts new file mode 100644 index 0000000..bcaa400 --- /dev/null +++ b/app-v3/src/core/i18n.ts @@ -0,0 +1,14 @@ +import { en } from '../i18n/en'; +import { pl } from '../i18n/pl'; +import type { Translations } from '../i18n/types'; + +export type { Translations }; +export type Lang = 'pl' | 'en'; + +const translations: Record = { pl, en }; + +export const lang: Lang = ( + (navigator.languages?.[0] ?? navigator.language ?? '').toLowerCase().startsWith('pl') +) ? 'pl' : 'en'; + +export const t: Translations = translations[lang]; diff --git a/app-v3/src/i18n/en.ts b/app-v3/src/i18n/en.ts new file mode 100644 index 0000000..1ef5ef4 --- /dev/null +++ b/app-v3/src/i18n/en.ts @@ -0,0 +1,140 @@ +import type { Translations } from './types'; + +export const en: Translations = { + menu: { + file: 'File', + edit: 'Edit', + image: 'Image', + layer: 'Layer', + }, + file: { + new: 'New Project', + open: 'Open Project\u2026', + save: 'Save Project', + importImage: 'Import Image\u2026', + exportPng: 'Export PNG', + newConfirm: 'New project? Unsaved changes will be lost.', + savedOk: 'Project saved.', + openedOk: 'Project opened.', + importedOk: 'Image imported.', + exportedOk: 'PNG exported.', + projectFilename: 'project.wdraft', + imageFilename: 'webdraft-image.png', + }, + edit: { + undo: 'Undo', + redo: 'Redo', + cut: 'Cut', + copy: 'Copy', + paste: 'Paste', + clearLayer: 'Clear Layer', + }, + image: { + resizeCanvas: 'Resize Canvas\u2026', + invertColors: 'Invert Colors', + mirrorH: 'Mirror Horizontal', + mirrorV: 'Mirror Vertical', + rotateLeft: 'Rotate Left', + rotateRight: 'Rotate Right', + widthPrompt: 'Width (px):', + heightPrompt: 'Height (px):', + resizedOk: (w, h) => `Canvas: ${w}\u00d7${h}`, + }, + layer: { + new: 'New Layer', + delete: 'Delete Layer', + moveUp: 'Move Layer Up', + moveDown: 'Move Layer Down', + addedOk: 'Layer added.', + deletedOk: 'Layer deleted.', + }, + toolbar: { + toolSelect: 'Select \u2014 S', + toolPencil: 'Pencil \u2014 P', + toolEraser: 'Eraser \u2014 E', + toolSampler: 'Sampler \u2014 C', + toolFillBucket: 'Fill bucket \u2014 B', + toolWeb: 'Web \u2014 W', + toolRectangle: 'Rectangle \u2014 R', + toolEllipse: 'Ellipse \u2014 O', + toolText: 'Text \u2014 T', + invertColors: 'Invert colors \u2014 I', + rotateLeft: 'Rotate left \u2014 ,', + rotateRight: 'Rotate right \u2014 .', + mirrorH: 'Mirror horizontally \u2014 H', + mirrorV: 'Mirror vertically \u2014 V', + size: 'Size', + webSensitivity: 'Web', + color: 'Color', + fillShapes: 'Fill shapes', + fillColor: 'Fill color', + fillOpacity: 'Fill opacity', + bucketTolerance: 'Bucket tolerance', + shadow: 'Shadow', + shadowColor: 'Shadow color', + shadowBlur: 'Shadow blur', + shadowX: 'Shadow X', + shadowY: 'Shadow Y', + textFont: 'Text font', + textAlignment: 'Text alignment', + bold: 'Bold', + italic: 'Italic', + clear: 'Clear', + undo: 'Undo \u2014 Ctrl+Z', + redo: 'Redo \u2014 Ctrl+Shift+Z', + copy: 'Copy \u2014 Ctrl+C', + cut: 'Cut \u2014 Ctrl+X', + paste: 'Paste \u2014 Ctrl+V', + width: 'Width', + height: 'Height', + resizeCanvas: 'Resize canvas', + uploadImage: 'Upload image', + camera: 'Camera', + exportPng: 'Export PNG', + saveProject: 'Save project', + openProject: 'Open project', + pngExportedOk: 'PNG exported.', + imageImportedOk: 'Image imported.', + canvasResizedOk: (w, h) => `Canvas resized to ${w} \u00d7 ${h}.`, + }, + layers: { + title: 'Layers', + addLayer: 'Add layer', + deleteLayer: 'Delete layer', + moveLayerUp: 'Move layer up', + moveLayerDown: 'Move layer down', + selectLayer: (name) => `Select ${name}`, + renameLayer: 'Rename layer', + hideLayer: 'Hide layer', + showLayer: 'Show layer', + }, + camera: { + title: 'Camera', + titleWithLabel: (label) => `Camera \u2014 ${label}`, + unavailable: 'Camera is unavailable in this browser.', + sepia: 'Sepia', + noise: 'Noise', + greyscale: 'Greyscale', + negative: 'Negative', + snap: 'Snap', + snapToCanvas: 'Snap to canvas', + applyToCanvas: 'Apply to canvas', + saveToFile: 'Save to file', + backToLive: 'Back to live', + frameAdded: 'Camera frame added.', + cameraError: 'Camera error.', + snapFilename: 'camera-snap.png', + }, + picker: { + addColor: 'Add current color', + restorePrevious: 'Click to restore previous color', + newColor: 'New:', + previousColor: 'Previous:', + range100: '0..100', + range255: '0..255', + }, + common: { + somethingWentWrong: 'Something went wrong.', + appTitle: 'WebDraft', + }, +}; diff --git a/app-v3/src/i18n/pl.ts b/app-v3/src/i18n/pl.ts new file mode 100644 index 0000000..e48625e --- /dev/null +++ b/app-v3/src/i18n/pl.ts @@ -0,0 +1,140 @@ +import type { Translations } from './types'; + +export const pl: Translations = { + menu: { + file: 'Plik', + edit: 'Edycja', + image: 'Obraz', + layer: 'Warstwa', + }, + file: { + new: 'Nowy projekt', + open: 'Otwórz projekt\u2026', + save: 'Zapisz projekt', + importImage: 'Importuj obraz\u2026', + exportPng: 'Eksportuj PNG', + newConfirm: 'Nowy projekt? Niezapisane zmiany zostaną utracone.', + savedOk: 'Projekt zapisany.', + openedOk: 'Projekt otwarty.', + importedOk: 'Obraz zaimportowany.', + exportedOk: 'PNG wyeksportowany.', + projectFilename: 'projekt.wdraft', + imageFilename: 'webdraft-obraz.png', + }, + edit: { + undo: 'Cofnij', + redo: 'Ponów', + cut: 'Wytnij', + copy: 'Kopiuj', + paste: 'Wklej', + clearLayer: 'Wyczyść warstwę', + }, + image: { + resizeCanvas: 'Zmień rozmiar płótna\u2026', + invertColors: 'Odwróć kolory', + mirrorH: 'Lustro poziome', + mirrorV: 'Lustro pionowe', + rotateLeft: 'Obróć w lewo', + rotateRight: 'Obróć w prawo', + widthPrompt: 'Szerokość (px):', + heightPrompt: 'Wysokość (px):', + resizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, + }, + layer: { + new: 'Nowa warstwa', + delete: 'Usuń warstwę', + moveUp: 'Przesuń warstwę w górę', + moveDown: 'Przesuń warstwę w dół', + addedOk: 'Warstwa dodana.', + deletedOk: 'Warstwa usunięta.', + }, + toolbar: { + toolSelect: 'Zaznaczanie — S', + toolPencil: 'Ołówek — P', + toolEraser: 'Gumka — E', + toolSampler: 'Próbnik — C', + toolFillBucket: 'Wiadro z farbą — B', + toolWeb: 'Sieć — W', + toolRectangle: 'Prostokąt — R', + toolEllipse: 'Elipsa — O', + toolText: 'Tekst — T', + invertColors: 'Odwróć kolory — I', + rotateLeft: 'Obróć w lewo — ,', + rotateRight: 'Obróć w prawo — .', + mirrorH: 'Lustro poziome — H', + mirrorV: 'Lustro pionowe — V', + size: 'Rozmiar', + webSensitivity: 'Sieć', + color: 'Kolor', + fillShapes: 'Wypełnij kształty', + fillColor: 'Kolor wypełnienia', + fillOpacity: 'Krycie wypełnienia', + bucketTolerance: 'Tolerancja', + shadow: 'Cień', + shadowColor: 'Kolor cienia', + shadowBlur: 'Rozmycie cienia', + shadowX: 'Cień X', + shadowY: 'Cień Y', + textFont: 'Czcionka', + textAlignment: 'Wyrównanie', + bold: 'Pogrubienie', + italic: 'Kursywa', + clear: 'Wyczyść', + undo: 'Cofnij — Ctrl+Z', + redo: 'Ponów — Ctrl+Shift+Z', + copy: 'Kopiuj — Ctrl+C', + cut: 'Wytnij — Ctrl+X', + paste: 'Wklej — Ctrl+V', + width: 'Szerokość', + height: 'Wysokość', + resizeCanvas: 'Zmień rozmiar płótna', + uploadImage: 'Wgraj obraz', + camera: 'Kamera', + exportPng: 'Eksportuj PNG', + saveProject: 'Zapisz projekt', + openProject: 'Otwórz projekt', + pngExportedOk: 'PNG wyeksportowany.', + imageImportedOk: 'Obraz zaimportowany.', + canvasResizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, + }, + layers: { + title: 'Warstwy', + addLayer: 'Dodaj warstwę', + deleteLayer: 'Usuń warstwę', + moveLayerUp: 'Przesuń warstwę w górę', + moveLayerDown: 'Przesuń warstwę w dół', + selectLayer: (name) => `Zaznacz ${name}`, + renameLayer: 'Zmień nazwę warstwy', + hideLayer: 'Ukryj warstwę', + showLayer: 'Pokaż warstwę', + }, + camera: { + title: 'Kamera', + titleWithLabel: (label) => `Kamera \u2014 ${label}`, + unavailable: 'Kamera jest niedostępna w tej przeglądarce.', + sepia: 'Sepia', + noise: 'Szum', + greyscale: 'Skala szarości', + negative: 'Negatyw', + snap: 'Zdjęcie', + snapToCanvas: 'Zdjęcie na płótno', + applyToCanvas: 'Zastosuj na płótno', + saveToFile: 'Zapisz do pliku', + backToLive: 'Wróć do podglądu', + frameAdded: 'Kadr z kamery dodany.', + cameraError: 'Błąd kamery.', + snapFilename: 'camera-snap.png', + }, + picker: { + addColor: 'Dodaj bieżący kolor', + restorePrevious: 'Kliknij, aby przywrócić poprzedni kolor', + newColor: 'Nowy:', + previousColor: 'Poprzedni:', + range100: '0..100', + range255: '0..255', + }, + common: { + somethingWentWrong: 'Coś poszło nie tak.', + appTitle: 'WebDraft', + }, +}; diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts new file mode 100644 index 0000000..2c91740 --- /dev/null +++ b/app-v3/src/i18n/types.ts @@ -0,0 +1,138 @@ +export type Translations = { + menu: { + file: string; + edit: string; + image: string; + layer: string; + }; + file: { + new: string; + open: string; + save: string; + importImage: string; + exportPng: string; + newConfirm: string; + savedOk: string; + openedOk: string; + importedOk: string; + exportedOk: string; + projectFilename: string; + imageFilename: string; + }; + edit: { + undo: string; + redo: string; + cut: string; + copy: string; + paste: string; + clearLayer: string; + }; + image: { + resizeCanvas: string; + invertColors: string; + mirrorH: string; + mirrorV: string; + rotateLeft: string; + rotateRight: string; + widthPrompt: string; + heightPrompt: string; + resizedOk: (w: number, h: number) => string; + }; + layer: { + new: string; + delete: string; + moveUp: string; + moveDown: string; + addedOk: string; + deletedOk: string; + }; + toolbar: { + toolSelect: string; + toolPencil: string; + toolEraser: string; + toolSampler: string; + toolFillBucket: string; + toolWeb: string; + toolRectangle: string; + toolEllipse: string; + toolText: string; + invertColors: string; + rotateLeft: string; + rotateRight: string; + mirrorH: string; + mirrorV: string; + size: string; + webSensitivity: string; + color: string; + fillShapes: string; + fillColor: string; + fillOpacity: string; + bucketTolerance: string; + shadow: string; + shadowColor: string; + shadowBlur: string; + shadowX: string; + shadowY: string; + textFont: string; + textAlignment: string; + bold: string; + italic: string; + clear: string; + undo: string; + redo: string; + copy: string; + cut: string; + paste: string; + width: string; + height: string; + resizeCanvas: string; + uploadImage: string; + camera: string; + exportPng: string; + saveProject: string; + openProject: string; + pngExportedOk: string; + imageImportedOk: string; + canvasResizedOk: (w: number, h: number) => string; + }; + layers: { + title: string; + addLayer: string; + deleteLayer: string; + moveLayerUp: string; + moveLayerDown: string; + selectLayer: (name: string) => string; + renameLayer: string; + hideLayer: string; + showLayer: string; + }; + camera: { + title: string; + titleWithLabel: (label: string) => string; + unavailable: string; + sepia: string; + noise: string; + greyscale: string; + negative: string; + snap: string; + snapToCanvas: string; + applyToCanvas: string; + saveToFile: string; + backToLive: string; + frameAdded: string; + cameraError: string; + snapFilename: string; + }; + picker: { + addColor: string; + restorePrevious: string; + newColor: string; + previousColor: string; + range100: string; + range255: string; + }; + common: { + somethingWentWrong: string; + appTitle: string; + }; +}; diff --git a/app-v3/src/ui/camera-panel.ts b/app-v3/src/ui/camera-panel.ts index ed45195..a92186e 100644 --- a/app-v3/src/ui/camera-panel.ts +++ b/app-v3/src/ui/camera-panel.ts @@ -1,3 +1,4 @@ +import {t} from '../core/i18n'; import type {WebDraftEditor} from '../core/webdraft-editor.ts'; import type {StatusReporter} from './status-toasts.ts'; @@ -59,7 +60,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): const mediaDevices = navigator.mediaDevices; if (!mediaDevices?.getUserMedia) { - status.show('Camera is unavailable in this browser.', 'error'); + status.show(t.camera.unavailable, 'error'); return; } @@ -85,7 +86,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): const title = document.createElement('span'); title.className = 'camera-panel__title'; - title.textContent = 'Camera'; + title.textContent = t.camera.title; const closeBtn = document.createElement('button'); closeBtn.type = 'button'; @@ -104,10 +105,10 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): filtersContainer.className = 'camera-panel__filters'; const filterDefs: Array<{label: string; key: keyof Filters}> = [ - {label: 'Sepia', key: 'sepia'}, - {label: 'Noise', key: 'noise'}, - {label: 'Greyscale', key: 'greyscale'}, - {label: 'Negative', key: 'negative'}, + {label: t.camera.sepia, key: 'sepia'}, + {label: t.camera.noise, key: 'noise'}, + {label: t.camera.greyscale, key: 'greyscale'}, + {label: t.camera.negative, key: 'negative'}, ]; for (const {label, key} of filterDefs) { @@ -123,31 +124,31 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): const snapBtn = document.createElement('button'); snapBtn.type = 'button'; snapBtn.className = 'command-button'; - snapBtn.textContent = 'Snap'; + snapBtn.textContent = t.camera.snap; snapBtn.addEventListener('click', doSnap); const snapToCanvasBtn = document.createElement('button'); snapToCanvasBtn.type = 'button'; snapToCanvasBtn.className = 'command-button'; - snapToCanvasBtn.textContent = 'Snap to canvas'; + snapToCanvasBtn.textContent = t.camera.snapToCanvas; snapToCanvasBtn.addEventListener('click', doSnapToCanvas); const applyBtn = document.createElement('button'); applyBtn.type = 'button'; applyBtn.className = 'command-button'; - applyBtn.textContent = 'Apply to canvas'; + applyBtn.textContent = t.camera.applyToCanvas; applyBtn.addEventListener('click', doApply); const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'command-button'; - saveBtn.textContent = 'Save to file'; + saveBtn.textContent = t.camera.saveToFile; saveBtn.addEventListener('click', doSave); const backBtn = document.createElement('button'); backBtn.type = 'button'; backBtn.className = 'command-button'; - backBtn.textContent = 'Back to live'; + backBtn.textContent = t.camera.backToLive; backBtn.addEventListener('click', resumeLive); showLiveActions(); @@ -179,7 +180,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): const track = stream.getVideoTracks()[0]; if (track?.label) { - title.textContent = `Camera — ${track.label}`; + title.textContent = t.camera.titleWithLabel(track.label); } previewCanvas.width = video.videoWidth; @@ -188,7 +189,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): startLiveLoop(); } catch (error) { closePanel(); - status.show(error instanceof Error ? error.message : 'Camera error.', 'error'); + status.show(error instanceof Error ? error.message : t.camera.cameraError, 'error'); } } @@ -234,7 +235,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): function doApply(): void { if (!snapshotCanvas) return; editor.importCanvasAsLayer(snapshotCanvas); - status.show('Camera frame added.', 'success'); + status.show(t.camera.frameAdded, 'success'); closePanel(); } @@ -245,7 +246,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'camera-snap.png'; + a.download = t.camera.snapFilename; a.click(); URL.revokeObjectURL(url); }); @@ -257,7 +258,7 @@ export function openCameraPanel(editor: WebDraftEditor, status: StatusReporter): tmp.height = previewCanvas.height; tmp.getContext('2d')?.drawImage(previewCanvas, 0, 0); editor.importCanvasAsLayer(tmp); - status.show('Camera frame added.', 'success'); + status.show(t.camera.frameAdded, 'success'); } function showLiveActions(): void { diff --git a/app-v3/src/ui/color-picker.ts b/app-v3/src/ui/color-picker.ts index b2a8734..80bc5d6 100644 --- a/app-v3/src/ui/color-picker.ts +++ b/app-v3/src/ui/color-picker.ts @@ -1,3 +1,5 @@ +import { t } from '../core/i18n'; + export type ColorPickerOptions = { label: string; value: string; @@ -161,8 +163,8 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerContr const rangeGroup = document.createElement('div'); rangeGroup.className = 'cp-btn-group'; - const btn100 = makeToggleBtn('0..100', rangeMax === 100, () => { rangeMax = 100; syncRangeButtons(); syncRightPanel(); }); - const btn255 = makeToggleBtn('0..255', rangeMax === 255, () => { rangeMax = 255; syncRangeButtons(); syncRightPanel(); }); + const btn100 = makeToggleBtn(t.picker.range100, rangeMax === 100, () => { rangeMax = 100; syncRangeButtons(); syncRightPanel(); }); + const btn255 = makeToggleBtn(t.picker.range255, rangeMax === 255, () => { rangeMax = 255; syncRangeButtons(); syncRightPanel(); }); rangeGroup.append(btn100, btn255); bar.append(rangeGroup); @@ -765,7 +767,7 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerContr const addBtn = document.createElement('button'); addBtn.className = 'cp-palette-add'; addBtn.textContent = '+'; - addBtn.title = 'Add current color'; + addBtn.title = t.picker.addColor; addBtn.addEventListener('click', () => { const palette = loadPalette(); if (!palette.includes(currentHex)) { @@ -821,7 +823,7 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerContr strip.className = 'cp-color-strip' + (isOld ? ' cp-color-strip--old' : ''); strip.style.backgroundColor = color; if (isOld) { - strip.title = 'Click to restore previous color'; + strip.title = t.picker.restorePrevious; strip.addEventListener('pointerdown', (e) => e.stopPropagation()); strip.addEventListener('click', () => { const parsed = hexToRgb(oldColor); @@ -838,8 +840,8 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerContr return row; }; - bar.append(make('New:', currentHex, false)); - bar.append(make('Previous:', oldColor, true)); + bar.append(make(t.picker.newColor, currentHex, false)); + bar.append(make(t.picker.previousColor, oldColor, true)); return bar; } diff --git a/app-v3/src/ui/layers-panel.ts b/app-v3/src/ui/layers-panel.ts index fc49d25..853d858 100644 --- a/app-v3/src/ui/layers-panel.ts +++ b/app-v3/src/ui/layers-panel.ts @@ -1,3 +1,4 @@ +import { t } from '../core/i18n'; import type { WebDraftEditor } from '../core/webdraft-editor'; export function createLayersPanel(editor: WebDraftEditor): HTMLElement { @@ -8,15 +9,15 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { header.className = 'panel-header'; const title = document.createElement('h2'); - title.textContent = 'Layers'; + title.textContent = t.layers.title; const actions = document.createElement('div'); actions.className = 'panel-actions'; - const addButton = createIconButton('Add layer', '+', () => editor.addLayer()); - const deleteButton = createIconButton('Delete layer', '-', () => editor.deleteActiveLayer()); - const upButton = createIconButton('Move layer up', '↑', () => editor.moveActiveLayerUp()); - const downButton = createIconButton('Move layer down', '↓', () => editor.moveActiveLayerDown()); + const addButton = createIconButton(t.layers.addLayer, '+', () => editor.addLayer()); + const deleteButton = createIconButton(t.layers.deleteLayer, '-', () => editor.deleteActiveLayer()); + const upButton = createIconButton(t.layers.moveLayerUp, '↑', () => editor.moveActiveLayerUp()); + const downButton = createIconButton(t.layers.moveLayerDown, '↓', () => editor.moveActiveLayerDown()); actions.append(addButton, deleteButton, upButton, downButton); header.append(title, actions); @@ -34,7 +35,7 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { const previewButton = document.createElement('button'); previewButton.type = 'button'; previewButton.className = 'layer-preview'; - previewButton.title = `Select ${layer.name}`; + previewButton.title = t.layers.selectLayer(layer.name); previewButton.addEventListener('click', () => editor.selectLayer(layer.id)); const previewImage = document.createElement('img'); @@ -45,7 +46,7 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { const renameInput = document.createElement('input'); renameInput.className = 'layer-name-input'; renameInput.value = layer.name; - renameInput.title = 'Rename layer'; + renameInput.title = t.layers.renameLayer; renameInput.addEventListener('focus', () => { renameInput.select(); }); @@ -61,7 +62,7 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { const visibilityButton = document.createElement('button'); visibilityButton.type = 'button'; visibilityButton.className = 'layer-visibility'; - visibilityButton.title = layer.visible ? 'Hide layer' : 'Show layer'; + visibilityButton.title = layer.visible ? t.layers.hideLayer : t.layers.showLayer; visibilityButton.textContent = layer.visible ? '●' : '○'; visibilityButton.addEventListener('click', () => editor.toggleLayerVisibility(layer.id)); diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts index 1de0668..7428b2e 100644 --- a/app-v3/src/ui/menu-bar.ts +++ b/app-v3/src/ui/menu-bar.ts @@ -1,4 +1,5 @@ import { parseWdraftBinary } from '../core/project-file'; +import { t } from '../core/i18n'; import type { WebDraftEditor } from '../core/webdraft-editor'; import type { StatusReporter } from './status-toasts'; import { getErrorMessage } from './status-toasts'; @@ -107,14 +108,14 @@ function createMenuButton(menu: Menu): HTMLButtonElement { export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): HTMLElement { const bar = document.createElement('nav'); bar.className = 'menu-bar'; - bar.setAttribute('aria-label', 'Main menu'); + bar.setAttribute('aria-label', t.menu.file); - const plikInput = document.createElement('input'); - plikInput.type = 'file'; - plikInput.accept = '.wdraft'; - plikInput.className = 'visually-hidden'; - plikInput.addEventListener('change', () => { - const file = plikInput.files?.[0]; + const projectInput = document.createElement('input'); + projectInput.type = 'file'; + projectInput.accept = '.wdraft'; + projectInput.className = 'visually-hidden'; + projectInput.addEventListener('change', () => { + const file = projectInput.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { @@ -122,11 +123,11 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H try { const parsed = parseWdraftBinary(reader.result as ArrayBuffer); await editor.importProject(parsed); - status.show('Projekt otwarty.', 'success'); + status.show(t.file.openedOk, 'success'); } catch (err) { status.show(getErrorMessage(err), 'error'); } finally { - plikInput.value = ''; + projectInput.value = ''; } })(); }; @@ -143,7 +144,7 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H void (async () => { try { await editor.importImage(file); - status.show('Obraz zaimportowany.', 'success'); + status.show(t.file.importedOk, 'success'); } catch (err) { status.show(getErrorMessage(err), 'error'); } finally { @@ -152,33 +153,33 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H })(); }); - bar.append(plikInput, imageInput); + bar.append(projectInput, imageInput); const menus: Menu[] = [ { - label: 'Plik', + label: t.menu.file, items: [ { type: 'action', - label: 'Nowy projekt', + label: t.file.new, shortcut: 'Ctrl+N', action: () => { - if (confirm('Nowy projekt? Niezapisane zmiany zostaną utracone.')) { + if (confirm(t.file.newConfirm)) { editor.clear(); - status.show('Nowy projekt.', 'success'); + status.show(t.file.new, 'success'); } }, }, { type: 'action', - label: 'Otwórz projekt…', + label: t.file.open, shortcut: 'Ctrl+O', - action: () => plikInput.click(), + action: () => projectInput.click(), }, { type: 'separator' }, { type: 'action', - label: 'Zapisz projekt', + label: t.file.save, shortcut: 'Ctrl+S', action: () => { void (async () => { @@ -187,10 +188,10 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'projekt.wdraft'; + a.download = t.file.projectFilename; a.click(); URL.revokeObjectURL(url); - status.show('Projekt zapisany.', 'success'); + status.show(t.file.savedOk, 'success'); } catch (err) { status.show(getErrorMessage(err), 'error'); } @@ -200,12 +201,12 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H { type: 'separator' }, { type: 'action', - label: 'Importuj obraz…', + label: t.file.importImage, action: () => imageInput.click(), }, { type: 'action', - label: 'Eksportuj PNG', + label: t.file.exportPng, shortcut: 'Ctrl+Shift+E', action: () => { void (async () => { @@ -214,10 +215,10 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'obraz.png'; + a.download = t.file.imageFilename; a.click(); URL.revokeObjectURL(url); - status.show('PNG wyeksportowany.', 'success'); + status.show(t.file.exportedOk, 'success'); } catch (err) { status.show(getErrorMessage(err), 'error'); } @@ -227,18 +228,18 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H ], }, { - label: 'Edycja', + label: t.menu.edit, items: [ { type: 'action', - label: 'Cofnij', + label: t.edit.undo, shortcut: 'Ctrl+Z', disabled: () => !editor.canUndo, action: () => editor.undo(), }, { type: 'action', - label: 'Ponów', + label: t.edit.redo, shortcut: 'Ctrl+Y', disabled: () => !editor.canRedo, action: () => editor.redo(), @@ -246,105 +247,105 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H { type: 'separator' }, { type: 'action', - label: 'Wytnij', + label: t.edit.cut, shortcut: 'Ctrl+X', action: () => editor.cutSelection(), }, { type: 'action', - label: 'Kopiuj', + label: t.edit.copy, shortcut: 'Ctrl+C', action: () => editor.copySelection(), }, { type: 'action', - label: 'Wklej', + label: t.edit.paste, shortcut: 'Ctrl+V', action: () => editor.pasteSelection(), }, { type: 'separator' }, { type: 'action', - label: 'Wyczyść warstwę', + label: t.edit.clearLayer, action: () => editor.clear(), }, ], }, { - label: 'Obraz', + label: t.menu.image, items: [ { type: 'action', - label: 'Zmień rozmiar płótna…', + label: t.image.resizeCanvas, action: () => { - const w = prompt('Szerokość (px):', '900'); - const h = prompt('Wysokość (px):', '620'); + const w = prompt(t.image.widthPrompt, '900'); + const h = prompt(t.image.heightPrompt, '620'); const nw = parseInt(w ?? '', 10); const nh = parseInt(h ?? '', 10); if (nw > 0 && nh > 0) { editor.resizeCanvas(nw, nh); - status.show(`Płótno: ${nw}×${nh}`, 'success'); + status.show(t.image.resizedOk(nw, nh), 'success'); } }, }, { type: 'separator' }, { type: 'action', - label: 'Odwróć kolory', + label: t.image.invertColors, action: () => editor.invertActiveLayer(), }, { type: 'action', - label: 'Lustro poziome', + label: t.image.mirrorH, action: () => editor.mirrorActiveLayer('horizontal'), }, { type: 'action', - label: 'Lustro pionowe', + label: t.image.mirrorV, action: () => editor.mirrorActiveLayer('vertical'), }, { type: 'separator' }, { type: 'action', - label: 'Obróć w lewo', + label: t.image.rotateLeft, action: () => editor.rotateActiveLayer('left'), }, { type: 'action', - label: 'Obróć w prawo', + label: t.image.rotateRight, action: () => editor.rotateActiveLayer('right'), }, ], }, { - label: 'Warstwa', + label: t.menu.layer, items: [ { type: 'action', - label: 'Nowa warstwa', + label: t.layer.new, action: () => { editor.addLayer(); - status.show('Warstwa dodana.', 'success'); + status.show(t.layer.addedOk, 'success'); }, }, { type: 'action', - label: 'Usuń warstwę', + label: t.layer.delete, disabled: () => editor.layers.length <= 1, action: () => { editor.deleteActiveLayer(); - status.show('Warstwa usunięta.', 'success'); + status.show(t.layer.deletedOk, 'success'); }, }, { type: 'separator' }, { type: 'action', - label: 'Przesuń warstwę w górę', + label: t.layer.moveUp, action: () => editor.moveActiveLayerUp(), }, { type: 'action', - label: 'Przesuń warstwę w dół', + label: t.layer.moveDown, action: () => editor.moveActiveLayerDown(), }, ], diff --git a/app-v3/src/ui/status-toasts.ts b/app-v3/src/ui/status-toasts.ts index 057578a..f5ecd93 100644 --- a/app-v3/src/ui/status-toasts.ts +++ b/app-v3/src/ui/status-toasts.ts @@ -1,3 +1,5 @@ +import { t } from '../core/i18n'; + export type StatusTone = 'info' | 'success' | 'error'; export type StatusReporter = { @@ -31,5 +33,5 @@ export function getErrorMessage(error: unknown): string { return error.message; } - return 'Something went wrong.'; + return t.common.somethingWentWrong; } diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index cd41c38..9712f29 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -1,4 +1,5 @@ import {parseWdraftBinary} from '../core/project-file'; +import {t} from '../core/i18n'; import {Tool} from '../core/types'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {openCameraPanel} from './camera-panel'; @@ -29,47 +30,47 @@ type ToolConfig = { const tools: ToolConfig[] = [ { id: Tool.Select, - label: 'Select — S', + label: t.toolbar.toolSelect, icon: '', }, { id: Tool.Pencil, - label: 'Pencil — P', + label: t.toolbar.toolPencil, icon: '', }, { id: Tool.Eraser, - label: 'Eraser — E', + label: t.toolbar.toolEraser, icon: '', }, { id: Tool.Sampler, - label: 'Sampler — C', + label: t.toolbar.toolSampler, icon: '', }, { id: Tool.FillBucket, - label: 'Fill bucket — B', + label: t.toolbar.toolFillBucket, icon: '', }, { id: Tool.Web, - label: 'Web — W', + label: t.toolbar.toolWeb, icon: '', }, { id: Tool.Rectangle, - label: 'Rectangle — R', + label: t.toolbar.toolRectangle, icon: '', }, { id: Tool.Ellipse, - label: 'Ellipse — O', + label: t.toolbar.toolEllipse, icon: '', }, { id: Tool.Text, - label: 'Text — T', + label: t.toolbar.toolText, icon: '', }, ]; @@ -123,27 +124,27 @@ export function createToolSection(editor: WebDraftEditor): ToolbarSection { const transformGroup = createGrid( 'toolbar__group', createEditButton( - 'Invert colors — I', + t.toolbar.invertColors, '', () => editor.invertActiveLayer(), ), createEditButton( - 'Rotate left — ,', + t.toolbar.rotateLeft, '', () => editor.rotateActiveLayer('left'), ), createEditButton( - 'Rotate right — .', + t.toolbar.rotateRight, '', () => editor.rotateActiveLayer('right'), ), createEditButton( - 'Mirror horizontally — H', + t.toolbar.mirrorH, '', () => editor.mirrorActiveLayer('horizontal'), ), createEditButton( - 'Mirror vertically — V', + t.toolbar.mirrorV, '', () => editor.mirrorActiveLayer('vertical'), ), @@ -161,7 +162,7 @@ export function createToolSection(editor: WebDraftEditor): ToolbarSection { export function createStyleSection(editor: WebDraftEditor): ToolbarSection { const sizeControl = createRangeControl({ - label: 'Size', + label: t.toolbar.size, min: 1, max: 120, value: editor.state.size, @@ -169,7 +170,7 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { }); const webSensitivityControl = createRangeControl({ - label: 'Web', + label: t.toolbar.webSensitivity, min: 20, max: 260, value: editor.state.webSensitivity, @@ -177,23 +178,23 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { }); const colorPicker = createColorPicker({ - label: 'Color', + label: t.toolbar.color, value: editor.state.color, onChange: (color) => editor.setColor(color), }); - const fillToggle = createCheckboxControl('Fill shapes', editor.state.fillEnabled, (enabled) => { + const fillToggle = createCheckboxControl(t.toolbar.fillShapes, editor.state.fillEnabled, (enabled) => { editor.setFillEnabled(enabled); }); const fillColorPicker = createColorPicker({ - label: 'Fill color', + label: t.toolbar.fillColor, value: editor.state.fillColor, onChange: (color) => editor.setFillColor(color), }); const fillOpacityControl = createRangeControl({ - label: 'Fill opacity', + label: t.toolbar.fillOpacity, min: 0, max: 100, value: editor.state.fillOpacity, @@ -201,7 +202,7 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { }); const fillToleranceControl = createRangeControl({ - label: 'Bucket tolerance', + label: t.toolbar.bucketTolerance, min: 0, max: 255, value: editor.state.fillTolerance, @@ -248,18 +249,18 @@ export function createStyleSection(editor: WebDraftEditor): ToolbarSection { } export function createShadowSection(editor: WebDraftEditor): ToolbarSection { - const shadowToggle = createCheckboxControl('Shadow', editor.state.shadowEnabled, (enabled) => { + const shadowToggle = createCheckboxControl(t.toolbar.shadow, editor.state.shadowEnabled, (enabled) => { editor.setShadowEnabled(enabled); }); const shadowColorPicker = createColorPicker({ - label: 'Shadow color', + label: t.toolbar.shadowColor, value: editor.state.shadowColor, onChange: (color) => editor.setShadowColor(color), }); const shadowBlurControl = createRangeControl({ - label: 'Shadow blur', + label: t.toolbar.shadowBlur, min: 0, max: 80, value: editor.state.shadowBlur, @@ -267,7 +268,7 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { }); const shadowXControl = createRangeControl({ - label: 'Shadow X', + label: t.toolbar.shadowX, min: -120, max: 120, value: editor.state.shadowOffsetX, @@ -275,7 +276,7 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { }); const shadowYControl = createRangeControl({ - label: 'Shadow Y', + label: t.toolbar.shadowY, min: -120, max: 120, value: editor.state.shadowOffsetY, @@ -312,18 +313,18 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { } export function createTextSection(editor: WebDraftEditor): ToolbarSection { - const fontSelect = createSelectControl('Text font', ['sans-serif', 'serif', 'monospace', 'cursive'], (font) => { + const fontSelect = createSelectControl(t.toolbar.textFont, ['sans-serif', 'serif', 'monospace', 'cursive'], (font) => { editor.setTextFontFamily(font); }); - const alignSelect = createSelectControl('Text alignment', ['left', 'center', 'right'] as const, (align) => { + const alignSelect = createSelectControl(t.toolbar.textAlignment, ['left', 'center', 'right'] as const, (align) => { editor.setTextAlign(align); }); - const textBoldToggle = createCheckboxControl('Bold', editor.state.textBold, (enabled) => { + const textBoldToggle = createCheckboxControl(t.toolbar.bold, editor.state.textBold, (enabled) => { editor.setTextBold(enabled); }); - const textItalicToggle = createCheckboxControl('Italic', editor.state.textItalic, (enabled) => { + const textItalicToggle = createCheckboxControl(t.toolbar.italic, editor.state.textItalic, (enabled) => { editor.setTextItalic(enabled); }); @@ -347,32 +348,32 @@ export function createTextSection(editor: WebDraftEditor): ToolbarSection { export function createEditSection(editor: WebDraftEditor): ToolbarSection { const clearButton = createEditButton( - 'Clear', + t.toolbar.clear, '', () => editor.clear(), ); const undoButton = createEditButton( - 'Undo — Ctrl+Z', + t.toolbar.undo, '', () => editor.undo(), ); const redoButton = createEditButton( - 'Redo — Ctrl+Shift+Z', + t.toolbar.redo, '', () => editor.redo(), ); const copyButton = createEditButton( - 'Copy — Ctrl+C', + t.toolbar.copy, '', () => editor.copySelection(), ); const cutButton = createEditButton( - 'Cut — Ctrl+X', + t.toolbar.cut, '', () => editor.cutSelection(), ); const pasteButton = createEditButton( - 'Paste — Ctrl+V', + t.toolbar.paste, '', () => editor.pasteSelection(), ); @@ -391,11 +392,11 @@ export function createEditSection(editor: WebDraftEditor): ToolbarSection { } export function createResizeSection(editor: WebDraftEditor, status?: StatusReporter): ToolbarSection { - const resizeWidthInput = createNumberInput('Width', editor.state.canvasWidth); - const resizeHeightInput = createNumberInput('Height', editor.state.canvasHeight); - const resizeButton = createCommandButton('Resize canvas', () => { + const resizeWidthInput = createNumberInput(t.toolbar.width, editor.state.canvasWidth); + const resizeHeightInput = createNumberInput(t.toolbar.height, editor.state.canvasHeight); + const resizeButton = createCommandButton(t.toolbar.resizeCanvas, () => { editor.resizeCanvas(Number(resizeWidthInput.value), Number(resizeHeightInput.value)); - status?.show(`Canvas resized to ${editor.state.canvasWidth} x ${editor.state.canvasHeight}.`, 'success'); + status?.show(t.toolbar.canvasResizedOk(editor.state.canvasWidth, editor.state.canvasHeight), 'success'); }); return { @@ -413,11 +414,11 @@ export function createFileSection( exportImage: () => Promise, status: StatusReporter, ): ToolbarSection { - const uploadButton = createCommandButton('Upload image', () => fileInput.click()); - const cameraButton = createCommandButton('Camera', () => { + const uploadButton = createCommandButton(t.toolbar.uploadImage, () => fileInput.click()); + const cameraButton = createCommandButton(t.toolbar.camera, () => { openCameraPanel(editor, status); }); - const exportButton = createCommandButton('Export PNG', () => { + const exportButton = createCommandButton(t.toolbar.exportPng, () => { void exportImage(); }); @@ -429,17 +430,17 @@ export function createFileSection( export function createProjectSection(editor: WebDraftEditor, status: StatusReporter): ToolbarSection { - const saveButton = createCommandButton('Save project', () => { + const saveButton = createCommandButton(t.toolbar.saveProject, () => { void (async () => { try { const blob = await editor.exportProject(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'project.wdraft'; + link.download = t.file.projectFilename; link.click(); URL.revokeObjectURL(url); - status.show('Project saved.', 'success'); + status.show(t.file.savedOk, 'success'); } catch (error) { status.show(getErrorMessage(error), 'error'); } @@ -459,7 +460,7 @@ export function createProjectSection(editor: WebDraftEditor, status: StatusRepor try { const parsed = parseWdraftBinary(reader.result as ArrayBuffer); await editor.importProject(parsed); - status.show('Project opened.', 'success'); + status.show(t.file.openedOk, 'success'); } catch (error) { status.show(getErrorMessage(error), 'error'); } finally { @@ -470,7 +471,7 @@ export function createProjectSection(editor: WebDraftEditor, status: StatusRepor reader.readAsArrayBuffer(file); }); - const openButton = createCommandButton('Open project', () => wdraftInput.click()); + const openButton = createCommandButton(t.toolbar.openProject, () => wdraftInput.click()); return { element: createToolbarSection(createGrid('toolbar__stack', saveButton, openButton, wdraftInput)), diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index f095fe8..3451865 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,3 +1,4 @@ +import {t} from '../core/i18n'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {bindKeyboardShortcuts} from './keyboard-shortcuts'; import type {StatusReporter} from './status-toasts'; @@ -27,10 +28,10 @@ export function createToolbar(editor: WebDraftEditor, status: StatusReporter): H const link = document.createElement('a'); link.href = url; - link.download = 'webdraft-image.png'; + link.download = t.file.imageFilename; link.click(); URL.revokeObjectURL(url); - status.show('PNG exported.', 'success'); + status.show(t.toolbar.pngExportedOk, 'success'); } catch (error) { status.show(getErrorMessage(error), 'error'); } @@ -72,7 +73,7 @@ function createFileInput(editor: WebDraftEditor, status: StatusReporter): HTMLIn try { await editor.importImage(file); - status.show('Image imported.', 'success'); + status.show(t.toolbar.imageImportedOk, 'success'); } catch (error) { status.show(getErrorMessage(error), 'error'); } finally { From cdeac3ef357bd2287acdb658652bfe4d78a93f1b Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 12:56:59 +0200 Subject: [PATCH 38/62] Split i18n files for better modularity and maintainability, restructure translations into separate namespaces, and migrate English and Polish translations into modularized files. Update `types.ts` to reflect the updated structure. --- app-v3/src/i18n/en.ts | 140 ----------------- app-v3/src/i18n/en/camera.ts | 19 +++ app-v3/src/i18n/en/common.ts | 6 + app-v3/src/i18n/en/edit.ts | 10 ++ app-v3/src/i18n/en/file.ts | 16 ++ app-v3/src/i18n/en/image.ts | 13 ++ app-v3/src/i18n/en/index.ts | 24 +++ app-v3/src/i18n/en/layer.ts | 10 ++ app-v3/src/i18n/en/layers.ts | 13 ++ app-v3/src/i18n/en/menu.ts | 8 + app-v3/src/i18n/en/picker.ts | 10 ++ app-v3/src/i18n/en/toolbar.ts | 51 ++++++ app-v3/src/i18n/pl.ts | 140 ----------------- app-v3/src/i18n/pl/camera.ts | 19 +++ app-v3/src/i18n/pl/common.ts | 6 + app-v3/src/i18n/pl/edit.ts | 10 ++ app-v3/src/i18n/pl/file.ts | 16 ++ app-v3/src/i18n/pl/image.ts | 13 ++ app-v3/src/i18n/pl/index.ts | 24 +++ app-v3/src/i18n/pl/layer.ts | 10 ++ app-v3/src/i18n/pl/layers.ts | 13 ++ app-v3/src/i18n/pl/menu.ts | 8 + app-v3/src/i18n/pl/picker.ts | 10 ++ app-v3/src/i18n/pl/toolbar.ts | 51 ++++++ app-v3/src/i18n/types.ts | 282 ++++++++++++++++++---------------- 25 files changed, 506 insertions(+), 416 deletions(-) delete mode 100644 app-v3/src/i18n/en.ts create mode 100644 app-v3/src/i18n/en/camera.ts create mode 100644 app-v3/src/i18n/en/common.ts create mode 100644 app-v3/src/i18n/en/edit.ts create mode 100644 app-v3/src/i18n/en/file.ts create mode 100644 app-v3/src/i18n/en/image.ts create mode 100644 app-v3/src/i18n/en/index.ts create mode 100644 app-v3/src/i18n/en/layer.ts create mode 100644 app-v3/src/i18n/en/layers.ts create mode 100644 app-v3/src/i18n/en/menu.ts create mode 100644 app-v3/src/i18n/en/picker.ts create mode 100644 app-v3/src/i18n/en/toolbar.ts delete mode 100644 app-v3/src/i18n/pl.ts create mode 100644 app-v3/src/i18n/pl/camera.ts create mode 100644 app-v3/src/i18n/pl/common.ts create mode 100644 app-v3/src/i18n/pl/edit.ts create mode 100644 app-v3/src/i18n/pl/file.ts create mode 100644 app-v3/src/i18n/pl/image.ts create mode 100644 app-v3/src/i18n/pl/index.ts create mode 100644 app-v3/src/i18n/pl/layer.ts create mode 100644 app-v3/src/i18n/pl/layers.ts create mode 100644 app-v3/src/i18n/pl/menu.ts create mode 100644 app-v3/src/i18n/pl/picker.ts create mode 100644 app-v3/src/i18n/pl/toolbar.ts diff --git a/app-v3/src/i18n/en.ts b/app-v3/src/i18n/en.ts deleted file mode 100644 index 1ef5ef4..0000000 --- a/app-v3/src/i18n/en.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Translations } from './types'; - -export const en: Translations = { - menu: { - file: 'File', - edit: 'Edit', - image: 'Image', - layer: 'Layer', - }, - file: { - new: 'New Project', - open: 'Open Project\u2026', - save: 'Save Project', - importImage: 'Import Image\u2026', - exportPng: 'Export PNG', - newConfirm: 'New project? Unsaved changes will be lost.', - savedOk: 'Project saved.', - openedOk: 'Project opened.', - importedOk: 'Image imported.', - exportedOk: 'PNG exported.', - projectFilename: 'project.wdraft', - imageFilename: 'webdraft-image.png', - }, - edit: { - undo: 'Undo', - redo: 'Redo', - cut: 'Cut', - copy: 'Copy', - paste: 'Paste', - clearLayer: 'Clear Layer', - }, - image: { - resizeCanvas: 'Resize Canvas\u2026', - invertColors: 'Invert Colors', - mirrorH: 'Mirror Horizontal', - mirrorV: 'Mirror Vertical', - rotateLeft: 'Rotate Left', - rotateRight: 'Rotate Right', - widthPrompt: 'Width (px):', - heightPrompt: 'Height (px):', - resizedOk: (w, h) => `Canvas: ${w}\u00d7${h}`, - }, - layer: { - new: 'New Layer', - delete: 'Delete Layer', - moveUp: 'Move Layer Up', - moveDown: 'Move Layer Down', - addedOk: 'Layer added.', - deletedOk: 'Layer deleted.', - }, - toolbar: { - toolSelect: 'Select \u2014 S', - toolPencil: 'Pencil \u2014 P', - toolEraser: 'Eraser \u2014 E', - toolSampler: 'Sampler \u2014 C', - toolFillBucket: 'Fill bucket \u2014 B', - toolWeb: 'Web \u2014 W', - toolRectangle: 'Rectangle \u2014 R', - toolEllipse: 'Ellipse \u2014 O', - toolText: 'Text \u2014 T', - invertColors: 'Invert colors \u2014 I', - rotateLeft: 'Rotate left \u2014 ,', - rotateRight: 'Rotate right \u2014 .', - mirrorH: 'Mirror horizontally \u2014 H', - mirrorV: 'Mirror vertically \u2014 V', - size: 'Size', - webSensitivity: 'Web', - color: 'Color', - fillShapes: 'Fill shapes', - fillColor: 'Fill color', - fillOpacity: 'Fill opacity', - bucketTolerance: 'Bucket tolerance', - shadow: 'Shadow', - shadowColor: 'Shadow color', - shadowBlur: 'Shadow blur', - shadowX: 'Shadow X', - shadowY: 'Shadow Y', - textFont: 'Text font', - textAlignment: 'Text alignment', - bold: 'Bold', - italic: 'Italic', - clear: 'Clear', - undo: 'Undo \u2014 Ctrl+Z', - redo: 'Redo \u2014 Ctrl+Shift+Z', - copy: 'Copy \u2014 Ctrl+C', - cut: 'Cut \u2014 Ctrl+X', - paste: 'Paste \u2014 Ctrl+V', - width: 'Width', - height: 'Height', - resizeCanvas: 'Resize canvas', - uploadImage: 'Upload image', - camera: 'Camera', - exportPng: 'Export PNG', - saveProject: 'Save project', - openProject: 'Open project', - pngExportedOk: 'PNG exported.', - imageImportedOk: 'Image imported.', - canvasResizedOk: (w, h) => `Canvas resized to ${w} \u00d7 ${h}.`, - }, - layers: { - title: 'Layers', - addLayer: 'Add layer', - deleteLayer: 'Delete layer', - moveLayerUp: 'Move layer up', - moveLayerDown: 'Move layer down', - selectLayer: (name) => `Select ${name}`, - renameLayer: 'Rename layer', - hideLayer: 'Hide layer', - showLayer: 'Show layer', - }, - camera: { - title: 'Camera', - titleWithLabel: (label) => `Camera \u2014 ${label}`, - unavailable: 'Camera is unavailable in this browser.', - sepia: 'Sepia', - noise: 'Noise', - greyscale: 'Greyscale', - negative: 'Negative', - snap: 'Snap', - snapToCanvas: 'Snap to canvas', - applyToCanvas: 'Apply to canvas', - saveToFile: 'Save to file', - backToLive: 'Back to live', - frameAdded: 'Camera frame added.', - cameraError: 'Camera error.', - snapFilename: 'camera-snap.png', - }, - picker: { - addColor: 'Add current color', - restorePrevious: 'Click to restore previous color', - newColor: 'New:', - previousColor: 'Previous:', - range100: '0..100', - range255: '0..255', - }, - common: { - somethingWentWrong: 'Something went wrong.', - appTitle: 'WebDraft', - }, -}; diff --git a/app-v3/src/i18n/en/camera.ts b/app-v3/src/i18n/en/camera.ts new file mode 100644 index 0000000..b0c1125 --- /dev/null +++ b/app-v3/src/i18n/en/camera.ts @@ -0,0 +1,19 @@ +import type {CameraTranslations} from '../types.ts'; + +export const cameraTranslations: CameraTranslations = { + title: 'Camera', + titleWithLabel: (label) => `Camera \u2014 ${label}`, + unavailable: 'Camera is unavailable in this browser.', + sepia: 'Sepia', + noise: 'Noise', + greyscale: 'Greyscale', + negative: 'Negative', + snap: 'Snap', + snapToCanvas: 'Snap to canvas', + applyToCanvas: 'Apply to canvas', + saveToFile: 'Save to file', + backToLive: 'Back to live', + frameAdded: 'Camera frame added.', + cameraError: 'Camera error.', + snapFilename: 'camera-snap.png', +}; diff --git a/app-v3/src/i18n/en/common.ts b/app-v3/src/i18n/en/common.ts new file mode 100644 index 0000000..bb27f1d --- /dev/null +++ b/app-v3/src/i18n/en/common.ts @@ -0,0 +1,6 @@ +import type {CommonTranslations} from '../types.ts'; + +export const commonTranslations: CommonTranslations = { + somethingWentWrong: 'Something went wrong.', + appTitle: 'WebDraft', +}; diff --git a/app-v3/src/i18n/en/edit.ts b/app-v3/src/i18n/en/edit.ts new file mode 100644 index 0000000..b0608ce --- /dev/null +++ b/app-v3/src/i18n/en/edit.ts @@ -0,0 +1,10 @@ +import type {EditTranslations} from '../types.ts'; + +export const editTranslations: EditTranslations = { + undo: 'Undo', + redo: 'Redo', + cut: 'Cut', + copy: 'Copy', + paste: 'Paste', + clearLayer: 'Clear Layer', +}; diff --git a/app-v3/src/i18n/en/file.ts b/app-v3/src/i18n/en/file.ts new file mode 100644 index 0000000..2016aa1 --- /dev/null +++ b/app-v3/src/i18n/en/file.ts @@ -0,0 +1,16 @@ +import type {FileTranslations} from '../types.ts'; + +export const fileTranslations: FileTranslations = { + new: 'New Project', + open: 'Open Project\u2026', + save: 'Save Project', + importImage: 'Import Image\u2026', + exportPng: 'Export PNG', + newConfirm: 'New project? Unsaved changes will be lost.', + savedOk: 'Project saved.', + openedOk: 'Project opened.', + importedOk: 'Image imported.', + exportedOk: 'PNG exported.', + projectFilename: 'project.wdraft', + imageFilename: 'webdraft-image.png', +}; diff --git a/app-v3/src/i18n/en/image.ts b/app-v3/src/i18n/en/image.ts new file mode 100644 index 0000000..24fdbd1 --- /dev/null +++ b/app-v3/src/i18n/en/image.ts @@ -0,0 +1,13 @@ +import type {ImageTranslations} from '../types.ts'; + +export const imageTranslations: ImageTranslations = { + resizeCanvas: 'Resize Canvas\u2026', + invertColors: 'Invert Colors', + mirrorH: 'Mirror Horizontal', + mirrorV: 'Mirror Vertical', + rotateLeft: 'Rotate Left', + rotateRight: 'Rotate Right', + widthPrompt: 'Width (px):', + heightPrompt: 'Height (px):', + resizedOk: (w, h) => `Canvas: ${w}\u00d7${h}`, +}; diff --git a/app-v3/src/i18n/en/index.ts b/app-v3/src/i18n/en/index.ts new file mode 100644 index 0000000..1213da1 --- /dev/null +++ b/app-v3/src/i18n/en/index.ts @@ -0,0 +1,24 @@ +import type {Translations} from '../types.ts'; +import {cameraTranslations} from './camera.ts'; +import {commonTranslations} from './common.ts'; +import {editTranslations} from './edit.ts'; +import {fileTranslations} from './file.ts'; +import {imageTranslations} from './image.ts'; +import {layerTranslations} from './layer.ts'; +import {layersTranslations} from './layers.ts'; +import {menuTranslations} from './menu.ts'; +import {pickerTranslations} from './picker.ts'; +import {toolbarTranslations} from './toolbar.ts'; + +export const en: Translations = { + menu: menuTranslations, + file: fileTranslations, + edit: editTranslations, + image: imageTranslations, + layer: layerTranslations, + toolbar: toolbarTranslations, + layers: layersTranslations, + camera: cameraTranslations, + picker: pickerTranslations, + common: commonTranslations, +}; diff --git a/app-v3/src/i18n/en/layer.ts b/app-v3/src/i18n/en/layer.ts new file mode 100644 index 0000000..9352c7a --- /dev/null +++ b/app-v3/src/i18n/en/layer.ts @@ -0,0 +1,10 @@ +import type {LayerTranslations} from '../types.ts'; + +export const layerTranslations: LayerTranslations = { + new: 'New Layer', + delete: 'Delete Layer', + moveUp: 'Move Layer Up', + moveDown: 'Move Layer Down', + addedOk: 'Layer added.', + deletedOk: 'Layer deleted.', +}; diff --git a/app-v3/src/i18n/en/layers.ts b/app-v3/src/i18n/en/layers.ts new file mode 100644 index 0000000..9f9b846 --- /dev/null +++ b/app-v3/src/i18n/en/layers.ts @@ -0,0 +1,13 @@ +import type {LayersTranslations} from '../types.ts'; + +export const layersTranslations: LayersTranslations = { + title: 'Layers', + addLayer: 'Add layer', + deleteLayer: 'Delete layer', + moveLayerUp: 'Move layer up', + moveLayerDown: 'Move layer down', + selectLayer: (name) => `Select ${name}`, + renameLayer: 'Rename layer', + hideLayer: 'Hide layer', + showLayer: 'Show layer', +}; diff --git a/app-v3/src/i18n/en/menu.ts b/app-v3/src/i18n/en/menu.ts new file mode 100644 index 0000000..fe31bd2 --- /dev/null +++ b/app-v3/src/i18n/en/menu.ts @@ -0,0 +1,8 @@ +import type {MenuTranslations} from '../types.ts'; + +export const menuTranslations: MenuTranslations = { + file: 'File', + edit: 'Edit', + image: 'Image', + layer: 'Layer', +}; diff --git a/app-v3/src/i18n/en/picker.ts b/app-v3/src/i18n/en/picker.ts new file mode 100644 index 0000000..4046edd --- /dev/null +++ b/app-v3/src/i18n/en/picker.ts @@ -0,0 +1,10 @@ +import type {PickerTranslations} from '../types.ts'; + +export const pickerTranslations: PickerTranslations = { + addColor: 'Add current color', + restorePrevious: 'Click to restore previous color', + newColor: 'New:', + previousColor: 'Previous:', + range100: '0..100', + range255: '0..255', +}; diff --git a/app-v3/src/i18n/en/toolbar.ts b/app-v3/src/i18n/en/toolbar.ts new file mode 100644 index 0000000..4ead470 --- /dev/null +++ b/app-v3/src/i18n/en/toolbar.ts @@ -0,0 +1,51 @@ +import type {ToolbarTranslations} from '../types.ts'; + +export const toolbarTranslations: ToolbarTranslations = { + toolSelect: 'Select \u2014 S', + toolPencil: 'Pencil \u2014 P', + toolEraser: 'Eraser \u2014 E', + toolSampler: 'Sampler \u2014 C', + toolFillBucket: 'Fill bucket \u2014 B', + toolWeb: 'Web \u2014 W', + toolRectangle: 'Rectangle \u2014 R', + toolEllipse: 'Ellipse \u2014 O', + toolText: 'Text \u2014 T', + invertColors: 'Invert colors \u2014 I', + rotateLeft: 'Rotate left \u2014 ,', + rotateRight: 'Rotate right \u2014 .', + mirrorH: 'Mirror horizontally \u2014 H', + mirrorV: 'Mirror vertically \u2014 V', + size: 'Size', + webSensitivity: 'Web', + color: 'Color', + fillShapes: 'Fill shapes', + fillColor: 'Fill color', + fillOpacity: 'Fill opacity', + bucketTolerance: 'Bucket tolerance', + shadow: 'Shadow', + shadowColor: 'Shadow color', + shadowBlur: 'Shadow blur', + shadowX: 'Shadow X', + shadowY: 'Shadow Y', + textFont: 'Text font', + textAlignment: 'Text alignment', + bold: 'Bold', + italic: 'Italic', + clear: 'Clear', + undo: 'Undo \u2014 Ctrl+Z', + redo: 'Redo \u2014 Ctrl+Shift+Z', + copy: 'Copy \u2014 Ctrl+C', + cut: 'Cut \u2014 Ctrl+X', + paste: 'Paste \u2014 Ctrl+V', + width: 'Width', + height: 'Height', + resizeCanvas: 'Resize canvas', + uploadImage: 'Upload image', + camera: 'Camera', + exportPng: 'Export PNG', + saveProject: 'Save project', + openProject: 'Open project', + pngExportedOk: 'PNG exported.', + imageImportedOk: 'Image imported.', + canvasResizedOk: (w, h) => `Canvas resized to ${w} \u00d7 ${h}.`, +}; diff --git a/app-v3/src/i18n/pl.ts b/app-v3/src/i18n/pl.ts deleted file mode 100644 index e48625e..0000000 --- a/app-v3/src/i18n/pl.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Translations } from './types'; - -export const pl: Translations = { - menu: { - file: 'Plik', - edit: 'Edycja', - image: 'Obraz', - layer: 'Warstwa', - }, - file: { - new: 'Nowy projekt', - open: 'Otwórz projekt\u2026', - save: 'Zapisz projekt', - importImage: 'Importuj obraz\u2026', - exportPng: 'Eksportuj PNG', - newConfirm: 'Nowy projekt? Niezapisane zmiany zostaną utracone.', - savedOk: 'Projekt zapisany.', - openedOk: 'Projekt otwarty.', - importedOk: 'Obraz zaimportowany.', - exportedOk: 'PNG wyeksportowany.', - projectFilename: 'projekt.wdraft', - imageFilename: 'webdraft-obraz.png', - }, - edit: { - undo: 'Cofnij', - redo: 'Ponów', - cut: 'Wytnij', - copy: 'Kopiuj', - paste: 'Wklej', - clearLayer: 'Wyczyść warstwę', - }, - image: { - resizeCanvas: 'Zmień rozmiar płótna\u2026', - invertColors: 'Odwróć kolory', - mirrorH: 'Lustro poziome', - mirrorV: 'Lustro pionowe', - rotateLeft: 'Obróć w lewo', - rotateRight: 'Obróć w prawo', - widthPrompt: 'Szerokość (px):', - heightPrompt: 'Wysokość (px):', - resizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, - }, - layer: { - new: 'Nowa warstwa', - delete: 'Usuń warstwę', - moveUp: 'Przesuń warstwę w górę', - moveDown: 'Przesuń warstwę w dół', - addedOk: 'Warstwa dodana.', - deletedOk: 'Warstwa usunięta.', - }, - toolbar: { - toolSelect: 'Zaznaczanie — S', - toolPencil: 'Ołówek — P', - toolEraser: 'Gumka — E', - toolSampler: 'Próbnik — C', - toolFillBucket: 'Wiadro z farbą — B', - toolWeb: 'Sieć — W', - toolRectangle: 'Prostokąt — R', - toolEllipse: 'Elipsa — O', - toolText: 'Tekst — T', - invertColors: 'Odwróć kolory — I', - rotateLeft: 'Obróć w lewo — ,', - rotateRight: 'Obróć w prawo — .', - mirrorH: 'Lustro poziome — H', - mirrorV: 'Lustro pionowe — V', - size: 'Rozmiar', - webSensitivity: 'Sieć', - color: 'Kolor', - fillShapes: 'Wypełnij kształty', - fillColor: 'Kolor wypełnienia', - fillOpacity: 'Krycie wypełnienia', - bucketTolerance: 'Tolerancja', - shadow: 'Cień', - shadowColor: 'Kolor cienia', - shadowBlur: 'Rozmycie cienia', - shadowX: 'Cień X', - shadowY: 'Cień Y', - textFont: 'Czcionka', - textAlignment: 'Wyrównanie', - bold: 'Pogrubienie', - italic: 'Kursywa', - clear: 'Wyczyść', - undo: 'Cofnij — Ctrl+Z', - redo: 'Ponów — Ctrl+Shift+Z', - copy: 'Kopiuj — Ctrl+C', - cut: 'Wytnij — Ctrl+X', - paste: 'Wklej — Ctrl+V', - width: 'Szerokość', - height: 'Wysokość', - resizeCanvas: 'Zmień rozmiar płótna', - uploadImage: 'Wgraj obraz', - camera: 'Kamera', - exportPng: 'Eksportuj PNG', - saveProject: 'Zapisz projekt', - openProject: 'Otwórz projekt', - pngExportedOk: 'PNG wyeksportowany.', - imageImportedOk: 'Obraz zaimportowany.', - canvasResizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, - }, - layers: { - title: 'Warstwy', - addLayer: 'Dodaj warstwę', - deleteLayer: 'Usuń warstwę', - moveLayerUp: 'Przesuń warstwę w górę', - moveLayerDown: 'Przesuń warstwę w dół', - selectLayer: (name) => `Zaznacz ${name}`, - renameLayer: 'Zmień nazwę warstwy', - hideLayer: 'Ukryj warstwę', - showLayer: 'Pokaż warstwę', - }, - camera: { - title: 'Kamera', - titleWithLabel: (label) => `Kamera \u2014 ${label}`, - unavailable: 'Kamera jest niedostępna w tej przeglądarce.', - sepia: 'Sepia', - noise: 'Szum', - greyscale: 'Skala szarości', - negative: 'Negatyw', - snap: 'Zdjęcie', - snapToCanvas: 'Zdjęcie na płótno', - applyToCanvas: 'Zastosuj na płótno', - saveToFile: 'Zapisz do pliku', - backToLive: 'Wróć do podglądu', - frameAdded: 'Kadr z kamery dodany.', - cameraError: 'Błąd kamery.', - snapFilename: 'camera-snap.png', - }, - picker: { - addColor: 'Dodaj bieżący kolor', - restorePrevious: 'Kliknij, aby przywrócić poprzedni kolor', - newColor: 'Nowy:', - previousColor: 'Poprzedni:', - range100: '0..100', - range255: '0..255', - }, - common: { - somethingWentWrong: 'Coś poszło nie tak.', - appTitle: 'WebDraft', - }, -}; diff --git a/app-v3/src/i18n/pl/camera.ts b/app-v3/src/i18n/pl/camera.ts new file mode 100644 index 0000000..2ccd9cb --- /dev/null +++ b/app-v3/src/i18n/pl/camera.ts @@ -0,0 +1,19 @@ +import type {CameraTranslations} from '../types.ts'; + +export const cameraTranslations: CameraTranslations = { + title: 'Kamera', + titleWithLabel: (label) => `Kamera \u2014 ${label}`, + unavailable: 'Kamera jest niedostępna w tej przeglądarce.', + sepia: 'Sepia', + noise: 'Szum', + greyscale: 'Skala szarości', + negative: 'Negatyw', + snap: 'Zdjęcie', + snapToCanvas: 'Zdjęcie na płótno', + applyToCanvas: 'Zastosuj na płótno', + saveToFile: 'Zapisz do pliku', + backToLive: 'Wróć do podglądu', + frameAdded: 'Kadr z kamery dodany.', + cameraError: 'Błąd kamery.', + snapFilename: 'camera-snap.png', +}; diff --git a/app-v3/src/i18n/pl/common.ts b/app-v3/src/i18n/pl/common.ts new file mode 100644 index 0000000..3802c54 --- /dev/null +++ b/app-v3/src/i18n/pl/common.ts @@ -0,0 +1,6 @@ +import type {CommonTranslations} from '../types.ts'; + +export const commonTranslations: CommonTranslations = { + somethingWentWrong: 'Coś poszło nie tak.', + appTitle: 'WebDraft', +}; diff --git a/app-v3/src/i18n/pl/edit.ts b/app-v3/src/i18n/pl/edit.ts new file mode 100644 index 0000000..02ac299 --- /dev/null +++ b/app-v3/src/i18n/pl/edit.ts @@ -0,0 +1,10 @@ +import type {EditTranslations} from '../types.ts'; + +export const editTranslations: EditTranslations = { + undo: 'Cofnij', + redo: 'Ponów', + cut: 'Wytnij', + copy: 'Kopiuj', + paste: 'Wklej', + clearLayer: 'Wyczyść warstwę', +}; diff --git a/app-v3/src/i18n/pl/file.ts b/app-v3/src/i18n/pl/file.ts new file mode 100644 index 0000000..555c7e9 --- /dev/null +++ b/app-v3/src/i18n/pl/file.ts @@ -0,0 +1,16 @@ +import type {FileTranslations} from '../types.ts'; + +export const fileTranslations: FileTranslations = { + new: 'Nowy projekt', + open: 'Otwórz projekt\u2026', + save: 'Zapisz projekt', + importImage: 'Importuj obraz\u2026', + exportPng: 'Eksportuj PNG', + newConfirm: 'Nowy projekt? Niezapisane zmiany zostaną utracone.', + savedOk: 'Projekt zapisany.', + openedOk: 'Projekt otwarty.', + importedOk: 'Obraz zaimportowany.', + exportedOk: 'PNG wyeksportowany.', + projectFilename: 'projekt.wdraft', + imageFilename: 'webdraft-obraz.png', +}; diff --git a/app-v3/src/i18n/pl/image.ts b/app-v3/src/i18n/pl/image.ts new file mode 100644 index 0000000..3bb8942 --- /dev/null +++ b/app-v3/src/i18n/pl/image.ts @@ -0,0 +1,13 @@ +import type {ImageTranslations} from '../types.ts'; + +export const imageTranslations: ImageTranslations = { + resizeCanvas: 'Zmień rozmiar płótna\u2026', + invertColors: 'Odwróć kolory', + mirrorH: 'Lustro poziome', + mirrorV: 'Lustro pionowe', + rotateLeft: 'Obróć w lewo', + rotateRight: 'Obróć w prawo', + widthPrompt: 'Szerokość (px):', + heightPrompt: 'Wysokość (px):', + resizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, +}; diff --git a/app-v3/src/i18n/pl/index.ts b/app-v3/src/i18n/pl/index.ts new file mode 100644 index 0000000..311d1a5 --- /dev/null +++ b/app-v3/src/i18n/pl/index.ts @@ -0,0 +1,24 @@ +import type {Translations} from '../types.ts'; +import {cameraTranslations} from './camera.ts'; +import {commonTranslations} from './common.ts'; +import {editTranslations} from './edit.ts'; +import {fileTranslations} from './file.ts'; +import {imageTranslations} from './image.ts'; +import {layerTranslations} from './layer.ts'; +import {layersTranslations} from './layers.ts'; +import {menuTranslations} from './menu.ts'; +import {pickerTranslations} from './picker.ts'; +import {toolbarTranslations} from './toolbar.ts'; + +export const pl: Translations = { + menu: menuTranslations, + file: fileTranslations, + edit: editTranslations, + image: imageTranslations, + layer: layerTranslations, + toolbar: toolbarTranslations, + layers: layersTranslations, + camera: cameraTranslations, + picker: pickerTranslations, + common: commonTranslations, +}; diff --git a/app-v3/src/i18n/pl/layer.ts b/app-v3/src/i18n/pl/layer.ts new file mode 100644 index 0000000..2a573eb --- /dev/null +++ b/app-v3/src/i18n/pl/layer.ts @@ -0,0 +1,10 @@ +import type {LayerTranslations} from '../types.ts'; + +export const layerTranslations: LayerTranslations = { + new: 'Nowa warstwa', + delete: 'Usuń warstwę', + moveUp: 'Przesuń warstwę w górę', + moveDown: 'Przesuń warstwę w dół', + addedOk: 'Warstwa dodana.', + deletedOk: 'Warstwa usunięta.', +}; diff --git a/app-v3/src/i18n/pl/layers.ts b/app-v3/src/i18n/pl/layers.ts new file mode 100644 index 0000000..5d9a362 --- /dev/null +++ b/app-v3/src/i18n/pl/layers.ts @@ -0,0 +1,13 @@ +import type {LayersTranslations} from '../types.ts'; + +export const layersTranslations: LayersTranslations = { + title: 'Warstwy', + addLayer: 'Dodaj warstwę', + deleteLayer: 'Usuń warstwę', + moveLayerUp: 'Przesuń warstwę w górę', + moveLayerDown: 'Przesuń warstwę w dół', + selectLayer: (name) => `Zaznacz ${name}`, + renameLayer: 'Zmień nazwę warstwy', + hideLayer: 'Ukryj warstwę', + showLayer: 'Pokaż warstwę', +}; diff --git a/app-v3/src/i18n/pl/menu.ts b/app-v3/src/i18n/pl/menu.ts new file mode 100644 index 0000000..a6d40b5 --- /dev/null +++ b/app-v3/src/i18n/pl/menu.ts @@ -0,0 +1,8 @@ +import type {MenuTranslations} from '../types.ts'; + +export const menuTranslations: MenuTranslations = { + file: 'Plik', + edit: 'Edycja', + image: 'Obraz', + layer: 'Warstwa', +}; diff --git a/app-v3/src/i18n/pl/picker.ts b/app-v3/src/i18n/pl/picker.ts new file mode 100644 index 0000000..c675374 --- /dev/null +++ b/app-v3/src/i18n/pl/picker.ts @@ -0,0 +1,10 @@ +import type {PickerTranslations} from '../types.ts'; + +export const pickerTranslations: PickerTranslations = { + addColor: 'Dodaj bieżący kolor', + restorePrevious: 'Kliknij, aby przywrócić poprzedni kolor', + newColor: 'Nowy:', + previousColor: 'Poprzedni:', + range100: '0..100', + range255: '0..255', +}; diff --git a/app-v3/src/i18n/pl/toolbar.ts b/app-v3/src/i18n/pl/toolbar.ts new file mode 100644 index 0000000..eeb856f --- /dev/null +++ b/app-v3/src/i18n/pl/toolbar.ts @@ -0,0 +1,51 @@ +import type {ToolbarTranslations} from '../types.ts'; + +export const toolbarTranslations: ToolbarTranslations = { + toolSelect: 'Zaznaczanie \u2014 S', + toolPencil: 'Ołówek \u2014 P', + toolEraser: 'Gumka \u2014 E', + toolSampler: 'Próbnik \u2014 C', + toolFillBucket: 'Wiadro z farbą \u2014 B', + toolWeb: 'Sieć \u2014 W', + toolRectangle: 'Prostokąt \u2014 R', + toolEllipse: 'Elipsa \u2014 O', + toolText: 'Tekst \u2014 T', + invertColors: 'Odwróć kolory \u2014 I', + rotateLeft: 'Obróć w lewo \u2014 ,', + rotateRight: 'Obróć w prawo \u2014 .', + mirrorH: 'Lustro poziome \u2014 H', + mirrorV: 'Lustro pionowe \u2014 V', + size: 'Rozmiar', + webSensitivity: 'Sieć', + color: 'Kolor', + fillShapes: 'Wypełnij kształty', + fillColor: 'Kolor wypełnienia', + fillOpacity: 'Krycie wypełnienia', + bucketTolerance: 'Tolerancja', + shadow: 'Cień', + shadowColor: 'Kolor cienia', + shadowBlur: 'Rozmycie cienia', + shadowX: 'Cień X', + shadowY: 'Cień Y', + textFont: 'Czcionka', + textAlignment: 'Wyrównanie', + bold: 'Pogrubienie', + italic: 'Kursywa', + clear: 'Wyczyść', + undo: 'Cofnij \u2014 Ctrl+Z', + redo: 'Ponów \u2014 Ctrl+Shift+Z', + copy: 'Kopiuj \u2014 Ctrl+C', + cut: 'Wytnij \u2014 Ctrl+X', + paste: 'Wklej \u2014 Ctrl+V', + width: 'Szerokość', + height: 'Wysokość', + resizeCanvas: 'Zmień rozmiar płótna', + uploadImage: 'Wgraj obraz', + camera: 'Kamera', + exportPng: 'Eksportuj PNG', + saveProject: 'Zapisz projekt', + openProject: 'Otwórz projekt', + pngExportedOk: 'PNG wyeksportowany.', + imageImportedOk: 'Obraz zaimportowany.', + canvasResizedOk: (w, h) => `Płótno zmienione: ${w}\u00d7${h}`, +}; diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts index 2c91740..d8a1b43 100644 --- a/app-v3/src/i18n/types.ts +++ b/app-v3/src/i18n/types.ts @@ -1,138 +1,148 @@ +export type MenuTranslations = { + file: string; + edit: string; + image: string; + layer: string; +}; +export type FileTranslations = { + new: string; + open: string; + save: string; + importImage: string; + exportPng: string; + newConfirm: string; + savedOk: string; + openedOk: string; + importedOk: string; + exportedOk: string; + projectFilename: string; + imageFilename: string; +}; +export type EditTranslations = { + undo: string; + redo: string; + cut: string; + copy: string; + paste: string; + clearLayer: string; +}; +export type ImageTranslations = { + resizeCanvas: string; + invertColors: string; + mirrorH: string; + mirrorV: string; + rotateLeft: string; + rotateRight: string; + widthPrompt: string; + heightPrompt: string; + resizedOk: (w: number, h: number) => string; +}; +export type LayerTranslations = { + new: string; + delete: string; + moveUp: string; + moveDown: string; + addedOk: string; + deletedOk: string; +}; +export type ToolbarTranslations = { + toolSelect: string; + toolPencil: string; + toolEraser: string; + toolSampler: string; + toolFillBucket: string; + toolWeb: string; + toolRectangle: string; + toolEllipse: string; + toolText: string; + invertColors: string; + rotateLeft: string; + rotateRight: string; + mirrorH: string; + mirrorV: string; + size: string; + webSensitivity: string; + color: string; + fillShapes: string; + fillColor: string; + fillOpacity: string; + bucketTolerance: string; + shadow: string; + shadowColor: string; + shadowBlur: string; + shadowX: string; + shadowY: string; + textFont: string; + textAlignment: string; + bold: string; + italic: string; + clear: string; + undo: string; + redo: string; + copy: string; + cut: string; + paste: string; + width: string; + height: string; + resizeCanvas: string; + uploadImage: string; + camera: string; + exportPng: string; + saveProject: string; + openProject: string; + pngExportedOk: string; + imageImportedOk: string; + canvasResizedOk: (w: number, h: number) => string; +}; +export type LayersTranslations = { + title: string; + addLayer: string; + deleteLayer: string; + moveLayerUp: string; + moveLayerDown: string; + selectLayer: (name: string) => string; + renameLayer: string; + hideLayer: string; + showLayer: string; +}; +export type CameraTranslations = { + title: string; + titleWithLabel: (label: string) => string; + unavailable: string; + sepia: string; + noise: string; + greyscale: string; + negative: string; + snap: string; + snapToCanvas: string; + applyToCanvas: string; + saveToFile: string; + backToLive: string; + frameAdded: string; + cameraError: string; + snapFilename: string; +}; +export type PickerTranslations = { + addColor: string; + restorePrevious: string; + newColor: string; + previousColor: string; + range100: string; + range255: string; +}; +export type CommonTranslations = { + somethingWentWrong: string; + appTitle: string; +}; export type Translations = { - menu: { - file: string; - edit: string; - image: string; - layer: string; - }; - file: { - new: string; - open: string; - save: string; - importImage: string; - exportPng: string; - newConfirm: string; - savedOk: string; - openedOk: string; - importedOk: string; - exportedOk: string; - projectFilename: string; - imageFilename: string; - }; - edit: { - undo: string; - redo: string; - cut: string; - copy: string; - paste: string; - clearLayer: string; - }; - image: { - resizeCanvas: string; - invertColors: string; - mirrorH: string; - mirrorV: string; - rotateLeft: string; - rotateRight: string; - widthPrompt: string; - heightPrompt: string; - resizedOk: (w: number, h: number) => string; - }; - layer: { - new: string; - delete: string; - moveUp: string; - moveDown: string; - addedOk: string; - deletedOk: string; - }; - toolbar: { - toolSelect: string; - toolPencil: string; - toolEraser: string; - toolSampler: string; - toolFillBucket: string; - toolWeb: string; - toolRectangle: string; - toolEllipse: string; - toolText: string; - invertColors: string; - rotateLeft: string; - rotateRight: string; - mirrorH: string; - mirrorV: string; - size: string; - webSensitivity: string; - color: string; - fillShapes: string; - fillColor: string; - fillOpacity: string; - bucketTolerance: string; - shadow: string; - shadowColor: string; - shadowBlur: string; - shadowX: string; - shadowY: string; - textFont: string; - textAlignment: string; - bold: string; - italic: string; - clear: string; - undo: string; - redo: string; - copy: string; - cut: string; - paste: string; - width: string; - height: string; - resizeCanvas: string; - uploadImage: string; - camera: string; - exportPng: string; - saveProject: string; - openProject: string; - pngExportedOk: string; - imageImportedOk: string; - canvasResizedOk: (w: number, h: number) => string; - }; - layers: { - title: string; - addLayer: string; - deleteLayer: string; - moveLayerUp: string; - moveLayerDown: string; - selectLayer: (name: string) => string; - renameLayer: string; - hideLayer: string; - showLayer: string; - }; - camera: { - title: string; - titleWithLabel: (label: string) => string; - unavailable: string; - sepia: string; - noise: string; - greyscale: string; - negative: string; - snap: string; - snapToCanvas: string; - applyToCanvas: string; - saveToFile: string; - backToLive: string; - frameAdded: string; - cameraError: string; - snapFilename: string; - }; - picker: { - addColor: string; - restorePrevious: string; - newColor: string; - previousColor: string; - range100: string; - range255: string; - }; - common: { - somethingWentWrong: string; - appTitle: string; - }; + menu: MenuTranslations; + file: FileTranslations; + edit: EditTranslations; + image: ImageTranslations; + layer: LayerTranslations; + toolbar: ToolbarTranslations; + layers: LayersTranslations; + camera: CameraTranslations; + picker: PickerTranslations; + common: CommonTranslations; }; From 77e0c82dd733b4a541d61718e4c08ceb1589daba Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 14:22:00 +0200 Subject: [PATCH 39/62] Add save dialog with project and PNG export options, keyboard shortcut integration, and localization support for English and Polish translations. --- app-v3/src/i18n/en/index.ts | 2 + app-v3/src/i18n/en/saveDialog.ts | 9 +++ app-v3/src/i18n/pl/index.ts | 2 + app-v3/src/i18n/pl/saveDialog.ts | 9 +++ app-v3/src/i18n/types.ts | 8 +++ app-v3/src/styles.css | 104 +++++++++++++++++++++++++++ app-v3/src/ui/keyboard-shortcuts.ts | 3 +- app-v3/src/ui/menu-bar.ts | 2 - app-v3/src/ui/save-dialog.ts | 105 ++++++++++++++++++++++++++++ app-v3/src/ui/toolbar.ts | 20 ++++++ 10 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 app-v3/src/i18n/en/saveDialog.ts create mode 100644 app-v3/src/i18n/pl/saveDialog.ts create mode 100644 app-v3/src/ui/save-dialog.ts diff --git a/app-v3/src/i18n/en/index.ts b/app-v3/src/i18n/en/index.ts index 1213da1..c42ce0f 100644 --- a/app-v3/src/i18n/en/index.ts +++ b/app-v3/src/i18n/en/index.ts @@ -8,6 +8,7 @@ import {layerTranslations} from './layer.ts'; import {layersTranslations} from './layers.ts'; import {menuTranslations} from './menu.ts'; import {pickerTranslations} from './picker.ts'; +import {saveDialogTranslations} from './saveDialog.ts'; import {toolbarTranslations} from './toolbar.ts'; export const en: Translations = { @@ -21,4 +22,5 @@ export const en: Translations = { camera: cameraTranslations, picker: pickerTranslations, common: commonTranslations, + saveDialog: saveDialogTranslations, }; diff --git a/app-v3/src/i18n/en/saveDialog.ts b/app-v3/src/i18n/en/saveDialog.ts new file mode 100644 index 0000000..cfefcbc --- /dev/null +++ b/app-v3/src/i18n/en/saveDialog.ts @@ -0,0 +1,9 @@ +import type {SaveDialogTranslations} from '../types.ts'; + +export const saveDialogTranslations: SaveDialogTranslations = { + title: 'Save', + exportPng: 'Export PNG', + saveProject: 'Save project', + rememberChoice: 'Remember decision', + cancel: 'Cancel', +}; diff --git a/app-v3/src/i18n/pl/index.ts b/app-v3/src/i18n/pl/index.ts index 311d1a5..21890b2 100644 --- a/app-v3/src/i18n/pl/index.ts +++ b/app-v3/src/i18n/pl/index.ts @@ -8,6 +8,7 @@ import {layerTranslations} from './layer.ts'; import {layersTranslations} from './layers.ts'; import {menuTranslations} from './menu.ts'; import {pickerTranslations} from './picker.ts'; +import {saveDialogTranslations} from './saveDialog.ts'; import {toolbarTranslations} from './toolbar.ts'; export const pl: Translations = { @@ -21,4 +22,5 @@ export const pl: Translations = { camera: cameraTranslations, picker: pickerTranslations, common: commonTranslations, + saveDialog: saveDialogTranslations, }; diff --git a/app-v3/src/i18n/pl/saveDialog.ts b/app-v3/src/i18n/pl/saveDialog.ts new file mode 100644 index 0000000..d065d64 --- /dev/null +++ b/app-v3/src/i18n/pl/saveDialog.ts @@ -0,0 +1,9 @@ +import type {SaveDialogTranslations} from '../types.ts'; + +export const saveDialogTranslations: SaveDialogTranslations = { + title: 'Zapisz', + exportPng: 'Eksportuj PNG', + saveProject: 'Zapisz projekt', + rememberChoice: 'Zapamiętaj decyzję', + cancel: 'Anuluj', +}; diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts index d8a1b43..5dc75ba 100644 --- a/app-v3/src/i18n/types.ts +++ b/app-v3/src/i18n/types.ts @@ -134,6 +134,13 @@ export type CommonTranslations = { somethingWentWrong: string; appTitle: string; }; +export type SaveDialogTranslations = { + title: string; + exportPng: string; + saveProject: string; + rememberChoice: string; + cancel: string; +}; export type Translations = { menu: MenuTranslations; file: FileTranslations; @@ -145,4 +152,5 @@ export type Translations = { camera: CameraTranslations; picker: PickerTranslations; common: CommonTranslations; + saveDialog: SaveDialogTranslations; }; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index dcd112b..a9cf4af 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -1119,3 +1119,107 @@ input[type="number"].cp-field-input::-webkit-inner-spin-button { grid-template-columns: 1fr 1fr; gap: 8px; } + +/* ── Save dialog ────────────────────────────────────────── */ + +.save-dialog-backdrop { + position: fixed; + inset: 0; + z-index: 1000; + background: rgb(0 0 0 / 55%); +} + +.save-dialog { + position: fixed; + z-index: 1001; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + background: #1e2126; + border: 1px solid #343941; + border-radius: 8px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; + box-shadow: 0 16px 48px rgb(0 0 0 / 55%); +} + +.save-dialog__title { + margin: 0; + font-size: 15px; + font-weight: 600; + color: #f4f7fb; +} + +.save-dialog__buttons { + display: flex; + flex-direction: column; + gap: 8px; +} + +.save-dialog__btn { + padding: 9px 14px; + border: 1px solid #3a4049; + border-radius: 5px; + background: #262b32; + color: #c8cdd4; + font-size: 13px; + cursor: pointer; + text-align: left; + transition: background 0.1s, color 0.1s; +} + +.save-dialog__btn:hover { + background: #2e3640; + color: #f4f7fb; +} + +.save-dialog__btn--primary { + background: #2e6fb7; + border-color: #2e6fb7; + color: #fff; +} + +.save-dialog__btn--primary:hover { + background: #3880cc; + border-color: #3880cc; +} + +.save-dialog__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.save-dialog__remember { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #8a9099; + cursor: pointer; + user-select: none; +} + +.save-dialog__remember input { + accent-color: #2e6fb7; + cursor: pointer; +} + +.save-dialog__cancel { + background: none; + border: none; + color: #8a9099; + font-size: 12px; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; +} + +.save-dialog__cancel:hover { + color: #c8cdd4; + background: #2c3038; +} diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index 67f9c9c..de76e00 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -4,6 +4,7 @@ import type {WebDraftEditor} from '../core/webdraft-editor'; type ShortcutOptions = { openImagePicker: () => void; exportImage: () => Promise; + onSave: () => void; }; export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutOptions): void { @@ -39,7 +40,7 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO if (isCommand && key === 's') { event.preventDefault(); - void options.exportImage(); + options.onSave(); return; } diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts index 7428b2e..1f21c65 100644 --- a/app-v3/src/ui/menu-bar.ts +++ b/app-v3/src/ui/menu-bar.ts @@ -180,7 +180,6 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H { type: 'action', label: t.file.save, - shortcut: 'Ctrl+S', action: () => { void (async () => { try { @@ -207,7 +206,6 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H { type: 'action', label: t.file.exportPng, - shortcut: 'Ctrl+Shift+E', action: () => { void (async () => { try { diff --git a/app-v3/src/ui/save-dialog.ts b/app-v3/src/ui/save-dialog.ts new file mode 100644 index 0000000..921d5fb --- /dev/null +++ b/app-v3/src/ui/save-dialog.ts @@ -0,0 +1,105 @@ +import { t } from '../core/i18n'; + +export type SaveAction = 'png' | 'project'; + +const STORAGE_KEY = 'webdraft-save-action'; + +function getStoredAction(): SaveAction | null { + const v = localStorage.getItem(STORAGE_KEY); + return v === 'png' || v === 'project' ? v : null; +} + +function storeAction(action: SaveAction): void { + localStorage.setItem(STORAGE_KEY, action); +} + +function clearStoredAction(): void { + localStorage.removeItem(STORAGE_KEY); +} + +export function openSaveDialog(onAction: (action: SaveAction) => void): void { + const stored = getStoredAction(); + if (stored) { + onAction(stored); + return; + } + + const backdrop = document.createElement('div'); + backdrop.className = 'save-dialog-backdrop'; + + const dialog = document.createElement('div'); + dialog.className = 'save-dialog'; + dialog.setAttribute('role', 'dialog'); + dialog.setAttribute('aria-modal', 'true'); + dialog.setAttribute('aria-label', t.saveDialog.title); + + const title = document.createElement('h2'); + title.className = 'save-dialog__title'; + title.textContent = t.saveDialog.title; + + const buttons = document.createElement('div'); + buttons.className = 'save-dialog__buttons'; + + const pngBtn = document.createElement('button'); + pngBtn.className = 'save-dialog__btn'; + pngBtn.textContent = t.saveDialog.exportPng; + + const projectBtn = document.createElement('button'); + projectBtn.className = 'save-dialog__btn save-dialog__btn--primary'; + projectBtn.textContent = t.saveDialog.saveProject; + + buttons.append(pngBtn, projectBtn); + + const footer = document.createElement('div'); + footer.className = 'save-dialog__footer'; + + const rememberLabel = document.createElement('label'); + rememberLabel.className = 'save-dialog__remember'; + + const rememberCheckbox = document.createElement('input'); + rememberCheckbox.type = 'checkbox'; + + const rememberText = document.createElement('span'); + rememberText.textContent = t.saveDialog.rememberChoice; + + rememberLabel.append(rememberCheckbox, rememberText); + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'save-dialog__cancel'; + cancelBtn.textContent = t.saveDialog.cancel; + + footer.append(rememberLabel, cancelBtn); + dialog.append(title, buttons, footer); + document.body.append(backdrop, dialog); + + function close(): void { + backdrop.remove(); + dialog.remove(); + document.removeEventListener('keydown', onKey); + } + + function choose(action: SaveAction): void { + if (rememberCheckbox.checked) { + storeAction(action); + } + close(); + onAction(action); + } + + function onKey(e: KeyboardEvent): void { + if (e.key === 'Escape') { + e.preventDefault(); + close(); + } + } + + pngBtn.addEventListener('click', () => choose('png')); + projectBtn.addEventListener('click', () => choose('project')); + cancelBtn.addEventListener('click', close); + backdrop.addEventListener('click', close); + document.addEventListener('keydown', onKey); + + projectBtn.focus(); +} + +export { clearStoredAction }; diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 3451865..557bd92 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -1,6 +1,7 @@ import {t} from '../core/i18n'; import type {WebDraftEditor} from '../core/webdraft-editor'; import {bindKeyboardShortcuts} from './keyboard-shortcuts'; +import {openSaveDialog} from './save-dialog'; import type {StatusReporter} from './status-toasts'; import {getErrorMessage} from './status-toasts'; import { @@ -51,9 +52,28 @@ export function createToolbar(editor: WebDraftEditor, status: StatusReporter): H toolbar.append(title, ...sections.map((section) => section.element)); + const saveProject = async () => { + try { + const blob = await editor.exportProject(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = t.file.projectFilename; + link.click(); + URL.revokeObjectURL(url); + status.show(t.file.savedOk, 'success'); + } catch (error) { + status.show(getErrorMessage(error), 'error'); + } + }; + bindKeyboardShortcuts(editor, { openImagePicker: () => fileInput.click(), exportImage, + onSave: () => openSaveDialog((action) => { + if (action === 'png') void exportImage(); + else void saveProject(); + }), }); return toolbar; From a1c70775ce0ff14df7b75fd1a79b14e65db645c8 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 14:33:14 +0200 Subject: [PATCH 40/62] Add dynamic layer name localization in English and Polish, refactor `layer-manager` to use i18n translation for layer names --- app-v3/src/core/layer-manager.ts | 5 ++++- app-v3/src/i18n/en/layers.ts | 1 + app-v3/src/i18n/pl/layers.ts | 1 + app-v3/src/i18n/types.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 2efa95d..3b45053 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -1,3 +1,4 @@ +import {t} from './i18n.ts'; import {SizeWithPosition} from './types.ts'; export type Layer = { @@ -91,9 +92,11 @@ export class LayerManager { canvas.className = 'drawing-layer'; canvas.dataset.layerId = id; + const name = t.layers.layerName(`${this.layerCount + 1}`) + const layer: Layer = { id, - name: `Layer ${this.layerCount + 1}`, + name, canvas, context, visible: true diff --git a/app-v3/src/i18n/en/layers.ts b/app-v3/src/i18n/en/layers.ts index 9f9b846..8b393c1 100644 --- a/app-v3/src/i18n/en/layers.ts +++ b/app-v3/src/i18n/en/layers.ts @@ -4,6 +4,7 @@ export const layersTranslations: LayersTranslations = { title: 'Layers', addLayer: 'Add layer', deleteLayer: 'Delete layer', + layerName: (suffix) => `Layer ${suffix}`, moveLayerUp: 'Move layer up', moveLayerDown: 'Move layer down', selectLayer: (name) => `Select ${name}`, diff --git a/app-v3/src/i18n/pl/layers.ts b/app-v3/src/i18n/pl/layers.ts index 5d9a362..9b8f640 100644 --- a/app-v3/src/i18n/pl/layers.ts +++ b/app-v3/src/i18n/pl/layers.ts @@ -4,6 +4,7 @@ export const layersTranslations: LayersTranslations = { title: 'Warstwy', addLayer: 'Dodaj warstwę', deleteLayer: 'Usuń warstwę', + layerName: (suffix) => `Warstwa ${suffix}`, moveLayerUp: 'Przesuń warstwę w górę', moveLayerDown: 'Przesuń warstwę w dół', selectLayer: (name) => `Zaznacz ${name}`, diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts index 5dc75ba..fe6de66 100644 --- a/app-v3/src/i18n/types.ts +++ b/app-v3/src/i18n/types.ts @@ -98,6 +98,7 @@ export type LayersTranslations = { title: string; addLayer: string; deleteLayer: string; + layerName: (suffix: string) => string; moveLayerUp: string; moveLayerDown: string; selectLayer: (name: string) => string; From d50783f3c14172f4b61af7cf396d9329bfae9e0e Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 10 Jun 2026 15:51:27 +0200 Subject: [PATCH 41/62] Add language selector with dynamic switching, state persistence, and session-based reload functionality. Update UI and i18n logic for improved localization handling. --- app-v3/src/core/i18n.ts | 17 ++++++-- app-v3/src/main.ts | 2 + app-v3/src/styles.css | 20 +++++++++ app-v3/src/ui/lang-state.ts | 29 +++++++++++++ app-v3/src/ui/menu-bar.ts | 81 ++++++++++++++++++++++++++++++++++++- 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 app-v3/src/ui/lang-state.ts diff --git a/app-v3/src/core/i18n.ts b/app-v3/src/core/i18n.ts index bcaa400..94990b3 100644 --- a/app-v3/src/core/i18n.ts +++ b/app-v3/src/core/i18n.ts @@ -5,10 +5,21 @@ import type { Translations } from '../i18n/types'; export type { Translations }; export type Lang = 'pl' | 'en'; +const LANG_KEY = 'webdraft-lang'; + const translations: Record = { pl, en }; -export const lang: Lang = ( - (navigator.languages?.[0] ?? navigator.language ?? '').toLowerCase().startsWith('pl') -) ? 'pl' : 'en'; +function detectLang(): Lang { + const stored = localStorage.getItem(LANG_KEY); + if (stored === 'pl' || stored === 'en') return stored; + return (navigator.languages?.[0] ?? navigator.language ?? '').toLowerCase().startsWith('pl') + ? 'pl' + : 'en'; +} + +export function setLang(next: Lang): void { + localStorage.setItem(LANG_KEY, next); +} +export const lang: Lang = detectLang(); export const t: Translations = translations[lang]; diff --git a/app-v3/src/main.ts b/app-v3/src/main.ts index 86619ff..b0ee3b0 100644 --- a/app-v3/src/main.ts +++ b/app-v3/src/main.ts @@ -3,6 +3,7 @@ import { createMenuBar } from './ui/menu-bar'; import { createRightPanel } from './ui/right-panel'; import { createStatusToasts } from './ui/status-toasts'; import { createToolbar } from './ui/toolbar'; +import { restoreStateAfterReload } from './ui/lang-state'; import './styles.css'; const app = document.querySelector('#app'); @@ -47,3 +48,4 @@ appWrap.prepend(createMenuBar(editor, statusToasts)); sidebar.append(createToolbar(editor, statusToasts)); rightPanel.append(createRightPanel(editor, statusToasts)); editor.mount(); +void restoreStateAfterReload(editor); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index a9cf4af..640120c 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -68,6 +68,26 @@ select { z-index: 100; } +.menu-lang-selector { + margin-left: auto; + display: flex; + align-items: stretch; +} + +.menu-lang-btn { + font-size: 12px; + letter-spacing: 0.02em; +} + +.menu-lang-dropdown { + min-width: 100px; +} + +.menu-dropdown__item--active button { + color: #fff; + background: #2e6fb7; +} + .menu-btn { display: flex; align-items: center; diff --git a/app-v3/src/ui/lang-state.ts b/app-v3/src/ui/lang-state.ts new file mode 100644 index 0000000..5ddb292 --- /dev/null +++ b/app-v3/src/ui/lang-state.ts @@ -0,0 +1,29 @@ +import { parseWdraftBinary } from '../core/project-file'; +import type { WebDraftEditor } from '../core/webdraft-editor'; + +const RESTORE_KEY = 'webdraft-lang-restore'; + +async function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve((reader.result as string).split(',')[1]); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +export async function saveStateForReload(editor: WebDraftEditor): Promise { + const blob = await editor.exportProject(); + const base64 = await blobToBase64(blob); + sessionStorage.setItem(RESTORE_KEY, base64); +} + +export async function restoreStateAfterReload(editor: WebDraftEditor): Promise { + const base64 = sessionStorage.getItem(RESTORE_KEY); + if (!base64) return; + sessionStorage.removeItem(RESTORE_KEY); + + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + const parsed = parseWdraftBinary(bytes.buffer as ArrayBuffer); + await editor.importProject(parsed); +} diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts index 1f21c65..57bcc0b 100644 --- a/app-v3/src/ui/menu-bar.ts +++ b/app-v3/src/ui/menu-bar.ts @@ -1,8 +1,10 @@ import { parseWdraftBinary } from '../core/project-file'; -import { t } from '../core/i18n'; +import { lang, setLang, t } from '../core/i18n'; +import type { Lang } from '../core/i18n'; import type { WebDraftEditor } from '../core/webdraft-editor'; import type { StatusReporter } from './status-toasts'; import { getErrorMessage } from './status-toasts'; +import { saveStateForReload } from './lang-state'; type MenuItem = | { type: 'action'; label: string; action: () => void; shortcut?: string; disabled?: () => boolean } @@ -105,6 +107,81 @@ function createMenuButton(menu: Menu): HTMLButtonElement { return btn; } +const LANG_LABELS: Record = { + pl: '🇵🇱 PL', + en: '🇬🇧 EN', +}; + +const ALL_LANGS: Lang[] = ['pl', 'en']; + +function createLangSelector(editor: WebDraftEditor): HTMLElement { + const wrapper = document.createElement('div'); + wrapper.className = 'menu-lang-selector'; + + const btn = document.createElement('button'); + btn.className = 'menu-btn menu-lang-btn'; + btn.textContent = LANG_LABELS[lang]; + btn.setAttribute('aria-haspopup', 'true'); + btn.setAttribute('aria-expanded', 'false'); + + let dropdown: HTMLElement | null = null; + + function closeLangMenu(): void { + dropdown?.remove(); + dropdown = null; + btn.classList.remove('menu-btn--active'); + btn.setAttribute('aria-expanded', 'false'); + } + + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (dropdown) { closeLangMenu(); return; } + + closeOpenMenu(); + + dropdown = document.createElement('ul'); + dropdown.className = 'menu-dropdown menu-lang-dropdown'; + dropdown.setAttribute('role', 'menu'); + + for (const l of ALL_LANGS) { + const li = document.createElement('li'); + li.setAttribute('role', 'none'); + li.className = 'menu-dropdown__item' + (l === lang ? ' menu-dropdown__item--active' : ''); + + const item = document.createElement('button'); + item.setAttribute('role', 'menuitem'); + item.tabIndex = -1; + item.textContent = LANG_LABELS[l]; + item.addEventListener('click', () => { + closeLangMenu(); + if (l !== lang) { + void saveStateForReload(editor).then(() => { + setLang(l); + location.reload(); + }); + } + }); + + li.append(item); + dropdown.append(li); + } + + const rect = btn.getBoundingClientRect(); + dropdown.style.left = `${rect.right}px`; + dropdown.style.top = `${rect.bottom}px`; + dropdown.style.transform = 'translateX(-100%)'; + + document.body.append(dropdown); + btn.classList.add('menu-btn--active'); + btn.setAttribute('aria-expanded', 'true'); + + document.addEventListener('click', closeLangMenu, { once: true }); + }); + + wrapper.append(btn); + return wrapper; +} + export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): HTMLElement { const bar = document.createElement('nav'); bar.className = 'menu-bar'; @@ -354,6 +431,8 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H bar.append(createMenuButton(menu)); } + bar.append(createLangSelector(editor)); + document.addEventListener('click', closeOpenMenu); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeOpenMenu(); From 51dc4c3297ea421142dc3ab59e90e8786ca7f1a4 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 10:05:55 +0200 Subject: [PATCH 42/62] Add support for layer positioning with `x` and `y` coordinates, integrate transformations with updated rotation logic, and enhance active layer styling and synchronization. --- app-v3/src/core/layer-manager.ts | 44 +++++++++++++++++++++++++++-- app-v3/src/core/layer-transforms.ts | 10 +++++-- app-v3/src/core/webdraft-editor.ts | 21 ++++++++++---- app-v3/src/styles.css | 10 +++++-- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 3b45053..8c300fa 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -7,6 +7,8 @@ export type Layer = { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; visible: boolean; + x: number; + y: number; }; export type LayerSummary = { @@ -31,6 +33,8 @@ export type LayerDocumentSnapshot = { type LayerSnapshotWithMetadata = LayerSnapshot & { name: string; visible: boolean; + x: number; + y: number; }; export class LayerManager { @@ -99,13 +103,17 @@ export class LayerManager { name, canvas, context, - visible: true + visible: true, + x: 0, + y: 0, }; this.layerCount += 1; this.layers.push(layer); this.activeLayerId = layer.id; this.root.append(canvas); + this.syncLayerStyle(layer); + this.updateActiveLayerClass(); return layer; } @@ -131,6 +139,9 @@ export class LayerManager { layer.canvas.height = height; layer.context.clearRect(0, 0, width, height); layer.context.drawImage(source, 0, 0); + layer.x = 0; + layer.y = 0; + this.syncLayerStyle(layer); } } @@ -142,6 +153,8 @@ export class LayerManager { layerId: layer.id, name: layer.name, visible: layer.visible, + x: layer.x, + y: layer.y, imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height), })), }; @@ -170,19 +183,24 @@ export class LayerManager { canvas.hidden = !layerSnapshot.visible; context.putImageData(layerSnapshot.imageData, 0, 0); - this.layers.push({ + const layer: Layer = { id: layerSnapshot.layerId, name: layerSnapshot.name, canvas, context, visible: layerSnapshot.visible, - }); + x: layerSnapshot.x ?? 0, + y: layerSnapshot.y ?? 0, + }; + this.syncLayerStyle(layer); + this.layers.push(layer); this.root.append(canvas); } this.activeLayerId = snapshot.layers.some((layer) => layer.layerId === snapshot.activeLayerId) ? snapshot.activeLayerId : this.layers[0]?.id ?? ''; + this.updateActiveLayerClass(); } @@ -201,6 +219,7 @@ export class LayerManager { selectLayer(id: string): void { this.assertLayer(id); this.activeLayerId = id; + this.updateActiveLayerClass(); } renameLayer(id: string, name: string): boolean { @@ -226,6 +245,7 @@ export class LayerManager { const nextLayer = this.layers[index] ?? this.layers[index - 1]; this.activeLayerId = nextLayer.id; + this.updateActiveLayerClass(); return removedLayer; } @@ -239,6 +259,7 @@ export class LayerManager { const nextVisibleLayer = [...this.layers].reverse().find((item) => item.visible); if (nextVisibleLayer) { this.activeLayerId = nextVisibleLayer.id; + this.updateActiveLayerClass(); } } } @@ -285,6 +306,23 @@ export class LayerManager { return layer; } + syncActiveLayerStyle(): void { + this.syncLayerStyle(this.activeLayer); + } + + private syncLayerStyle(layer: Layer): void { + layer.canvas.style.left = `${layer.x}px`; + layer.canvas.style.top = `${layer.y}px`; + layer.canvas.style.width = `${layer.canvas.width}px`; + layer.canvas.style.height = `${layer.canvas.height}px`; + } + + private updateActiveLayerClass(): void { + for (const layer of this.layers) { + layer.canvas.classList.toggle('drawing-layer--active', layer.id === this.activeLayerId); + } + } + private swapLayers(firstIndex: number, secondIndex: number): void { const firstLayer = this.layers[firstIndex]; const secondLayer = this.layers[secondIndex]; diff --git a/app-v3/src/core/layer-transforms.ts b/app-v3/src/core/layer-transforms.ts index 760febf..1c18123 100644 --- a/app-v3/src/core/layer-transforms.ts +++ b/app-v3/src/core/layer-transforms.ts @@ -20,14 +20,18 @@ export function invertPixelBuffer(buffer: PixelBuffer): PixelBuffer { } export function rotatePixelBuffer(buffer: PixelBuffer, direction: RotationDirection): PixelBuffer { - const output = createEmptyPixelBuffer(buffer); + const output: PixelBuffer = { + width: buffer.height, + height: buffer.width, + data: new Uint8ClampedArray(buffer.width * buffer.height * 4), + }; for (let y = 0; y < buffer.height; y += 1) { for (let x = 0; x < buffer.width; x += 1) { const target = direction === 'right' - ? {x: buffer.width - 1 - y, y: x} - : {x: y, y: buffer.height - 1 - x}; + ? {x: buffer.height - 1 - y, y: x} + : {x: y, y: buffer.width - 1 - x}; copyPixel(buffer, output, x, y, target.x, target.y); } diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index e1013a0..726bce2 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -356,6 +356,8 @@ export class WebDraftEditor extends EventTarget { layerId: l.id, name: l.name, visible: l.visible, + x: 0, + y: 0, imageData: imageDataList[i], })), }; @@ -380,7 +382,7 @@ export class WebDraftEditor extends EventTarget { canvas.height = this.state.canvasHeight; for (const layer of this.layerManager.visibleLayers) { - context.drawImage(layer.canvas, 0, 0); + context.drawImage(layer.canvas, layer.x, layer.y); } const blob = await new Promise((resolve) => { @@ -534,11 +536,18 @@ export class WebDraftEditor extends EventTarget { this.clearSelection(); const before = this.captureHistorySnapshot(); - const {canvas, context} = this.layerManager.activeLayer; - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - const transformed = rotatePixelBuffer(imageData, direction); - + const layer = this.layerManager.activeLayer; + const {canvas, context} = layer; + const oldW = canvas.width; + const oldH = canvas.height; + const transformed = rotatePixelBuffer(context.getImageData(0, 0, oldW, oldH), direction); + + canvas.width = transformed.width; + canvas.height = transformed.height; context.putImageData(createImageData(transformed), 0, 0); + layer.x = Math.round(layer.x + (oldW - oldH) / 2); + layer.y = Math.round(layer.y + (oldH - oldW) / 2); + this.layerManager.syncActiveLayerStyle(); this.pushHistory(before, this.captureHistorySnapshot()); } @@ -752,7 +761,7 @@ export class WebDraftEditor extends EventTarget { canvas.height = this.state.canvasHeight; for (const layer of this.layerManager.visibleLayers) { - context.drawImage(layer.canvas, 0, 0); + context.drawImage(layer.canvas, layer.x, layer.y); } const [red, green, blue, alpha] = context.getImageData(x, y, 1, 1).data; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 640120c..2873d8f 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -205,10 +205,9 @@ select { background-size: 20px 20px; border: 1px solid #4b89ff; box-shadow: 0 24px 80px rgb(0 0 0 / 30%); - overflow: hidden; + overflow: visible; } -.drawing-layer, .shape-preview-layer, .event-layer { position: absolute; @@ -218,10 +217,17 @@ select { } .drawing-layer { + position: absolute; + left: 0; + top: 0; z-index: 1; pointer-events: none; } +.drawing-layer--active { + outline: 1px dashed rgba(75, 137, 255, 0.7); +} + .shape-preview-layer { z-index: 5; pointer-events: none; From 830bf2a629148b43877f75eff1ff66d0843d3298 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 10:55:07 +0200 Subject: [PATCH 43/62] Add layer resizing functionality with UI dialog, enhance canvas resizing logic with selective layer updates, and update i18n translations and styles for resize support. --- app-v3/src/core/webdraft-editor.ts | 46 ++++++++++++- app-v3/src/i18n/en/image.ts | 1 + app-v3/src/i18n/en/layer.ts | 2 + app-v3/src/i18n/pl/image.ts | 1 + app-v3/src/i18n/pl/layer.ts | 2 + app-v3/src/i18n/types.ts | 3 + app-v3/src/styles.css | 106 ++++++++++++++++++++++++++++ app-v3/src/ui/menu-bar.ts | 36 +++++++--- app-v3/src/ui/resize-dialog.ts | 107 +++++++++++++++++++++++++++++ 9 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 app-v3/src/ui/resize-dialog.ts diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 726bce2..3277207 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -118,6 +118,22 @@ export class WebDraftEditor extends EventTarget { return this.clipboard !== null; } + get canvasWidth(): number { + return this.state.canvasWidth; + } + + get canvasHeight(): number { + return this.state.canvasHeight; + } + + get activeLayerWidth(): number { + return this.layerManager.activeLayer.canvas.width; + } + + get activeLayerHeight(): number { + return this.layerManager.activeLayer.canvas.height; + } + setTool(tool: Tool): void { this.commitTextInput(); this.state.activeTool = tool; @@ -207,7 +223,7 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } - resizeCanvas(width: number, height: number): void { + resizeCanvas(width: number, height: number, resizeLayersToo = true): void { this.commitTextInput(); this.clearSelection(); @@ -221,12 +237,38 @@ export class WebDraftEditor extends EventTarget { const before = this.captureHistorySnapshot(); this.state.canvasWidth = nextWidth; this.state.canvasHeight = nextHeight; - this.layerManager.resizeLayers(nextWidth, nextHeight); + if (resizeLayersToo) this.layerManager.resizeLayers(nextWidth, nextHeight); this.applyCanvasSize(); this.clipboard = null; this.pushHistory(before, this.captureHistorySnapshot()); } + resizeActiveLayer(width: number, height: number): void { + this.commitTextInput(); + this.clearSelection(); + + const nextWidth = Math.min(Math.max(Math.round(width), 1), 8192); + const nextHeight = Math.min(Math.max(Math.round(height), 1), 8192); + + const before = this.captureHistorySnapshot(); + const layer = this.layerManager.activeLayer; + const source = document.createElement('canvas'); + const sourceCtx = source.getContext('2d'); + + if (!sourceCtx) throw new Error('Canvas 2D context is unavailable.'); + + source.width = layer.canvas.width; + source.height = layer.canvas.height; + sourceCtx.drawImage(layer.canvas, 0, 0); + layer.canvas.width = nextWidth; + layer.canvas.height = nextHeight; + layer.context.clearRect(0, 0, nextWidth, nextHeight); + layer.context.drawImage(source, 0, 0); + this.layerManager.syncActiveLayerStyle(); + this.pushHistory(before, this.captureHistorySnapshot()); + this.dispatchChange(); + } + clear(): void { const before = this.captureHistorySnapshot(); this.layerManager.clearActiveLayer(); diff --git a/app-v3/src/i18n/en/image.ts b/app-v3/src/i18n/en/image.ts index 24fdbd1..b5e1312 100644 --- a/app-v3/src/i18n/en/image.ts +++ b/app-v3/src/i18n/en/image.ts @@ -10,4 +10,5 @@ export const imageTranslations: ImageTranslations = { widthPrompt: 'Width (px):', heightPrompt: 'Height (px):', resizedOk: (w, h) => `Canvas: ${w}\u00d7${h}`, + applyToAllLayers: 'Apply to all layers', }; diff --git a/app-v3/src/i18n/en/layer.ts b/app-v3/src/i18n/en/layer.ts index 9352c7a..5315a0a 100644 --- a/app-v3/src/i18n/en/layer.ts +++ b/app-v3/src/i18n/en/layer.ts @@ -7,4 +7,6 @@ export const layerTranslations: LayerTranslations = { moveDown: 'Move Layer Down', addedOk: 'Layer added.', deletedOk: 'Layer deleted.', + resize: 'Resize Layer\u2026', + layerResizedOk: (w, h) => `Layer: ${w}\u00d7${h}`, }; diff --git a/app-v3/src/i18n/pl/image.ts b/app-v3/src/i18n/pl/image.ts index 3bb8942..29ad349 100644 --- a/app-v3/src/i18n/pl/image.ts +++ b/app-v3/src/i18n/pl/image.ts @@ -10,4 +10,5 @@ export const imageTranslations: ImageTranslations = { widthPrompt: 'Szerokość (px):', heightPrompt: 'Wysokość (px):', resizedOk: (w, h) => `Płótno: ${w}\u00d7${h}`, + applyToAllLayers: 'Zastosuj do wszystkich warstw', }; diff --git a/app-v3/src/i18n/pl/layer.ts b/app-v3/src/i18n/pl/layer.ts index 2a573eb..742e236 100644 --- a/app-v3/src/i18n/pl/layer.ts +++ b/app-v3/src/i18n/pl/layer.ts @@ -7,4 +7,6 @@ export const layerTranslations: LayerTranslations = { moveDown: 'Przesuń warstwę w dół', addedOk: 'Warstwa dodana.', deletedOk: 'Warstwa usunięta.', + resize: 'Zmień rozmiar warstwy\u2026', + layerResizedOk: (w, h) => `Warstwa: ${w}\u00d7${h}`, }; diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts index fe6de66..b53c33d 100644 --- a/app-v3/src/i18n/types.ts +++ b/app-v3/src/i18n/types.ts @@ -36,6 +36,7 @@ export type ImageTranslations = { widthPrompt: string; heightPrompt: string; resizedOk: (w: number, h: number) => string; + applyToAllLayers: string; }; export type LayerTranslations = { new: string; @@ -44,6 +45,8 @@ export type LayerTranslations = { moveDown: string; addedOk: string; deletedOk: string; + resize: string; + layerResizedOk: (w: number, h: number) => string; }; export type ToolbarTranslations = { toolSelect: string; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 2873d8f..4cfe03f 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -1249,3 +1249,109 @@ input[type="number"].cp-field-input::-webkit-inner-spin-button { color: #c8cdd4; background: #2c3038; } + +.resize-dlg-backdrop { + position: fixed; + inset: 0; + z-index: 1000; + background: rgb(0 0 0 / 55%); +} + +.resize-dlg { + position: fixed; + z-index: 1001; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 280px; + background: #1e2126; + border: 1px solid #343941; + border-radius: 8px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 16px 48px rgb(0 0 0 / 55%); +} + +.resize-dlg__title { + margin: 0; + font-size: 15px; + font-weight: 600; + color: #f4f7fb; +} + +.resize-dlg__field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.resize-dlg__label { + font-size: 12px; + color: #8a9099; + display: flex; + flex-direction: column; + gap: 4px; +} + +.resize-dlg__input { + padding: 6px 8px; + background: #262b32; + border: 1px solid #3a4049; + border-radius: 5px; + color: #f4f7fb; + font-size: 13px; + width: 100%; + box-sizing: border-box; +} + +.resize-dlg__input:focus { + outline: none; + border-color: #4b89ff; +} + +.resize-dlg__checkbox-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #8a9099; + cursor: pointer; +} + +.resize-dlg__actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 2px; +} + +.resize-dlg__btn { + padding: 6px 16px; + border-radius: 5px; + font-size: 13px; + cursor: pointer; + border: 1px solid transparent; +} + +.resize-dlg__btn--primary { + background: #2e6fb7; + border-color: #2e6fb7; + color: #fff; +} + +.resize-dlg__btn--primary:hover { + background: #3a7fc7; +} + +.resize-dlg__btn--secondary { + background: transparent; + border-color: #3a4049; + color: #8a9099; +} + +.resize-dlg__btn--secondary:hover { + color: #c8cdd4; + background: #2c3038; +} diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts index 57bcc0b..ee0024f 100644 --- a/app-v3/src/ui/menu-bar.ts +++ b/app-v3/src/ui/menu-bar.ts @@ -5,6 +5,7 @@ import type { WebDraftEditor } from '../core/webdraft-editor'; import type { StatusReporter } from './status-toasts'; import { getErrorMessage } from './status-toasts'; import { saveStateForReload } from './lang-state'; +import { openResizeDialog } from './resize-dialog'; type MenuItem = | { type: 'action'; label: string; action: () => void; shortcut?: string; disabled?: () => boolean } @@ -353,14 +354,17 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H type: 'action', label: t.image.resizeCanvas, action: () => { - const w = prompt(t.image.widthPrompt, '900'); - const h = prompt(t.image.heightPrompt, '620'); - const nw = parseInt(w ?? '', 10); - const nh = parseInt(h ?? '', 10); - if (nw > 0 && nh > 0) { - editor.resizeCanvas(nw, nh); - status.show(t.image.resizedOk(nw, nh), 'success'); - } + openResizeDialog({ + title: t.image.resizeCanvas, + width: editor.canvasWidth, + height: editor.canvasHeight, + checkboxLabel: t.image.applyToAllLayers, + checkboxChecked: true, + onConfirm: (w, h, all) => { + editor.resizeCanvas(w, h, all); + status.show(t.image.resizedOk(w, h), 'success'); + }, + }); }, }, { type: 'separator' }, @@ -423,6 +427,22 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H label: t.layer.moveDown, action: () => editor.moveActiveLayerDown(), }, + { type: 'separator' }, + { + type: 'action', + label: t.layer.resize, + action: () => { + openResizeDialog({ + title: t.layer.resize, + width: editor.activeLayerWidth, + height: editor.activeLayerHeight, + onConfirm: (w, h) => { + editor.resizeActiveLayer(w, h); + status.show(t.layer.layerResizedOk(w, h), 'success'); + }, + }); + }, + }, ], }, ]; diff --git a/app-v3/src/ui/resize-dialog.ts b/app-v3/src/ui/resize-dialog.ts new file mode 100644 index 0000000..f1c0ed8 --- /dev/null +++ b/app-v3/src/ui/resize-dialog.ts @@ -0,0 +1,107 @@ +import { t } from '../core/i18n'; + +type ResizeDialogOptions = { + title: string; + width: number; + height: number; + checkboxLabel?: string; + checkboxChecked?: boolean; + onConfirm: (w: number, h: number, all: boolean) => void; +}; + +export function openResizeDialog(opts: ResizeDialogOptions): void { + const backdrop = document.createElement('div'); + backdrop.className = 'resize-dlg-backdrop'; + + const dialog = document.createElement('div'); + dialog.className = 'resize-dlg'; + dialog.setAttribute('role', 'dialog'); + dialog.setAttribute('aria-modal', 'true'); + + const titleEl = document.createElement('h2'); + titleEl.className = 'resize-dlg__title'; + titleEl.textContent = opts.title; + + function makeField(labelText: string, defaultValue: number): { row: HTMLElement; input: HTMLInputElement } { + const row = document.createElement('div'); + row.className = 'resize-dlg__field'; + const lbl = document.createElement('label'); + lbl.className = 'resize-dlg__label'; + lbl.textContent = labelText; + const input = document.createElement('input'); + input.type = 'number'; + input.className = 'resize-dlg__input'; + input.min = '1'; + input.max = '8192'; + input.value = String(defaultValue); + lbl.append(input); + row.append(lbl); + return { row, input }; + } + + const { row: wRow, input: wInput } = makeField(t.image.widthPrompt, opts.width); + const { row: hRow, input: hInput } = makeField(t.image.heightPrompt, opts.height); + + let checkboxEl: HTMLInputElement | null = null; + + const actions = document.createElement('div'); + actions.className = 'resize-dlg__actions'; + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'resize-dlg__btn resize-dlg__btn--secondary'; + cancelBtn.textContent = t.saveDialog.cancel; + cancelBtn.type = 'button'; + + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'resize-dlg__btn resize-dlg__btn--primary'; + confirmBtn.textContent = 'OK'; + confirmBtn.type = 'button'; + + actions.append(cancelBtn, confirmBtn); + + dialog.append(titleEl, wRow, hRow); + + if (opts.checkboxLabel !== undefined) { + const checkboxRow = document.createElement('label'); + checkboxRow.className = 'resize-dlg__checkbox-row'; + checkboxEl = document.createElement('input'); + checkboxEl.type = 'checkbox'; + checkboxEl.checked = opts.checkboxChecked ?? true; + const lbl = document.createElement('span'); + lbl.textContent = opts.checkboxLabel; + checkboxRow.append(checkboxEl, lbl); + dialog.append(checkboxRow); + } + + dialog.append(actions); + backdrop.append(dialog); + document.body.append(backdrop); + + wInput.focus(); + wInput.select(); + + function close(): void { + backdrop.remove(); + } + + function confirm(): void { + const w = parseInt(wInput.value, 10); + const h = parseInt(hInput.value, 10); + if (w > 0 && h > 0) { + close(); + opts.onConfirm(w, h, checkboxEl?.checked ?? false); + } + } + + cancelBtn.addEventListener('click', close); + confirmBtn.addEventListener('click', confirm); + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) close(); + }); + + dialog.addEventListener('keydown', (e) => { + if (e.key === 'Escape') close(); + if (e.key === 'Enter') confirm(); + }); +} From 667162be69d795f690436ea97e8036349a5270ee Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 12:40:49 +0200 Subject: [PATCH 44/62] Normalize coordinate handling by introducing `toLayerPoint` utility; adjust tools and rendering methods for layer-relative positioning. --- app-v3/src/core/webdraft-editor.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 3277207..4ff579e 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -650,7 +650,7 @@ export class WebDraftEditor extends EventTarget { } if (this.state.activeTool === Tool.FillBucket) { - this.fillActiveLayer(this.lastPoint); + this.fillActiveLayer(this.toLayerPoint(this.lastPoint)); this.isDrawing = false; this.lastPoint = null; return; @@ -664,12 +664,13 @@ export class WebDraftEditor extends EventTarget { } if (this.state.activeTool === Tool.Web) { - this.webPoints = [this.lastPoint]; - drawPoint(this.layerManager.activeLayer.context, this.lastPoint, this.state); + const lp = this.toLayerPoint(this.lastPoint); + this.webPoints = [lp]; + drawPoint(this.layerManager.activeLayer.context, lp, this.state); return; } - drawPoint(this.layerManager.activeLayer.context, this.lastPoint, this.state); + drawPoint(this.layerManager.activeLayer.context, this.toLayerPoint(this.lastPoint), this.state); }); this.eventLayer.addEventListener('pointermove', (event) => { @@ -695,12 +696,12 @@ export class WebDraftEditor extends EventTarget { } if (this.state.activeTool === Tool.Web) { - this.webPoints = drawWebLine(this.layerManager.activeLayer.context, nextPoint, this.webPoints, this.state); + this.webPoints = drawWebLine(this.layerManager.activeLayer.context, this.toLayerPoint(nextPoint), this.webPoints, this.state); this.lastPoint = nextPoint; return; } - drawLine(this.layerManager.activeLayer.context, this.lastPoint, nextPoint, this.state); + drawLine(this.layerManager.activeLayer.context, this.toLayerPoint(this.lastPoint), this.toLayerPoint(nextPoint), this.state); this.lastPoint = nextPoint; }); @@ -740,6 +741,11 @@ export class WebDraftEditor extends EventTarget { }); } + private toLayerPoint(p: Point): Point { + const { x, y } = this.layerManager.activeLayer; + return { x: p.x - x, y: p.y - y }; + } + private getPoint(event: PointerEvent): Point { const rect = this.eventLayer.getBoundingClientRect(); @@ -938,9 +944,10 @@ export class WebDraftEditor extends EventTarget { } const before = this.captureHistorySnapshot(); - const {context} = this.layerManager.activeLayer; + const { context, x: lx, y: ly } = this.layerManager.activeLayer; + const layerBounds = { x: bounds.x - lx, y: bounds.y - ly, width: bounds.width, height: bounds.height }; - drawText(context, value, bounds, this.state); + drawText(context, value, layerBounds, this.state); this.pushHistory(before, this.captureHistorySnapshot()); this.textBounds = null; } @@ -1008,7 +1015,7 @@ export class WebDraftEditor extends EventTarget { } const {context} = this.layerManager.activeLayer; - drawShape(context, getBounds(this.shapeStartPoint, point), this.state); + drawShape(context, getBounds(this.toLayerPoint(this.shapeStartPoint), this.toLayerPoint(point)), this.state); this.clearPreview(); } From 6d268476b9bac772561fe39fd4e3a87f3f0e0a84 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 14:42:23 +0200 Subject: [PATCH 45/62] Add support for text layers, including creation, editing, and text persistence; update serialization/deserialization logic to support new layer type, enhance i18n translations, UI, and styling accordingly. --- app-v3/src/core/layer-manager.ts | 12 +++- app-v3/src/core/project-file.ts | 92 ++++++++++++++++-------------- app-v3/src/core/types.ts | 5 ++ app-v3/src/core/webdraft-editor.ts | 62 ++++++++++++++++++-- app-v3/src/i18n/en/layers.ts | 1 + app-v3/src/i18n/pl/layers.ts | 1 + app-v3/src/i18n/types.ts | 1 + app-v3/src/styles.css | 15 +++++ app-v3/src/ui/layers-panel.ts | 8 +++ 9 files changed, 145 insertions(+), 52 deletions(-) diff --git a/app-v3/src/core/layer-manager.ts b/app-v3/src/core/layer-manager.ts index 8c300fa..2fd8d8a 100644 --- a/app-v3/src/core/layer-manager.ts +++ b/app-v3/src/core/layer-manager.ts @@ -1,5 +1,7 @@ import {t} from './i18n.ts'; -import {SizeWithPosition} from './types.ts'; +import type {SizeWithPosition, TextLayerData} from './types.ts'; + +export type { TextLayerData }; export type Layer = { id: string; @@ -9,6 +11,7 @@ export type Layer = { visible: boolean; x: number; y: number; + textData?: TextLayerData; }; export type LayerSummary = { @@ -17,6 +20,7 @@ export type LayerSummary = { active: boolean; preview: string; visible: boolean; + isTextLayer: boolean; }; type LayerSnapshot = { @@ -35,6 +39,7 @@ type LayerSnapshotWithMetadata = LayerSnapshot & { visible: boolean; x: number; y: number; + textData?: TextLayerData; }; export class LayerManager { @@ -68,7 +73,8 @@ export class LayerManager { name: layer.name, active: layer.id === this.activeLayerId, preview: layer.canvas.toDataURL('image/png'), - visible: layer.visible + visible: layer.visible, + isTextLayer: !!layer.textData, })) .reverse(); } @@ -155,6 +161,7 @@ export class LayerManager { visible: layer.visible, x: layer.x, y: layer.y, + textData: layer.textData, imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height), })), }; @@ -191,6 +198,7 @@ export class LayerManager { visible: layerSnapshot.visible, x: layerSnapshot.x ?? 0, y: layerSnapshot.y ?? 0, + textData: layerSnapshot.textData, }; this.syncLayerStyle(layer); this.layers.push(layer); diff --git a/app-v3/src/core/project-file.ts b/app-v3/src/core/project-file.ts index d9aaa03..4ade4a3 100644 --- a/app-v3/src/core/project-file.ts +++ b/app-v3/src/core/project-file.ts @@ -1,14 +1,17 @@ -export const WDRAFT_VERSION = 2; +export const WDRAFT_VERSION = 3; export const WDRAFT_MAGIC = [0x57, 0x44, 0x46, 0x54] as const; // "WDFT" export const WDRAFT_MIME_TYPE = 'application/x-webdraft'; export const WDRAFT_EXTENSION = '.wdraft'; +import type { TextLayerData } from './types'; + export type WdraftLayerMeta = { id: string; name: string; visible: boolean; width: number; height: number; + textData?: TextLayerData; }; export type WdraftFileMeta = { @@ -33,6 +36,7 @@ export function serializeWdraftBinary(meta: WdraftFileMeta, pngBuffers: Uint8Arr const layerStrings = meta.layers.map((l) => ({ id: enc.encode(l.id), name: enc.encode(l.name), + textDataJson: l.textData ? enc.encode(JSON.stringify(l.textData)) : null, })); // Calculate total size @@ -46,13 +50,15 @@ export function serializeWdraftBinary(meta: WdraftFileMeta, pngBuffers: Uint8Arr 2; // layer entries count for (let i = 0; i < meta.layers.length; i++) { + const td = layerStrings[i].textDataJson; size += 2 + layerStrings[i].id.length + 2 + layerStrings[i].name.length + 1 + // visible 4 + // width 4 + // height - 4 + pngBuffers[i].length; // png data length + data + 4 + pngBuffers[i].length + // png data length + data + 2 + (td ? td.length : 0); // textData length + data (0 = none) } const buffer = new ArrayBuffer(size); @@ -61,62 +67,46 @@ export function serializeWdraftBinary(meta: WdraftFileMeta, pngBuffers: Uint8Arr let offset = 0; - // Magic - for (const byte of WDRAFT_MAGIC) { - bytes[offset++] = byte; - } - - // Version + for (const byte of WDRAFT_MAGIC) bytes[offset++] = byte; bytes[offset++] = WDRAFT_VERSION; - // Canvas dimensions - view.setUint32(offset, meta.canvasWidth, true); - offset += 4; - view.setUint32(offset, meta.canvasHeight, true); - offset += 4; + view.setUint32(offset, meta.canvasWidth, true); offset += 4; + view.setUint32(offset, meta.canvasHeight, true); offset += 4; + view.setUint32(offset, meta.layerCount, true); offset += 4; - // Internal layer counter - view.setUint32(offset, meta.layerCount, true); - offset += 4; - - // Active layer ID - view.setUint16(offset, activeLayerIdBytes.length, true); - offset += 2; - bytes.set(activeLayerIdBytes, offset); - offset += activeLayerIdBytes.length; + view.setUint16(offset, activeLayerIdBytes.length, true); offset += 2; + bytes.set(activeLayerIdBytes, offset); offset += activeLayerIdBytes.length; - // Layer entries count - view.setUint16(offset, meta.layers.length, true); - offset += 2; + view.setUint16(offset, meta.layers.length, true); offset += 2; - // Layers for (let i = 0; i < meta.layers.length; i++) { const layer = meta.layers[i]; const idBytes = layerStrings[i].id; const nameBytes = layerStrings[i].name; + const td = layerStrings[i].textDataJson; const png = pngBuffers[i]; - view.setUint16(offset, idBytes.length, true); - offset += 2; - bytes.set(idBytes, offset); - offset += idBytes.length; + view.setUint16(offset, idBytes.length, true); offset += 2; + bytes.set(idBytes, offset); offset += idBytes.length; - view.setUint16(offset, nameBytes.length, true); - offset += 2; - bytes.set(nameBytes, offset); - offset += nameBytes.length; + view.setUint16(offset, nameBytes.length, true); offset += 2; + bytes.set(nameBytes, offset); offset += nameBytes.length; bytes[offset++] = layer.visible ? 1 : 0; - view.setUint32(offset, layer.width, true); - offset += 4; - view.setUint32(offset, layer.height, true); - offset += 4; + view.setUint32(offset, layer.width, true); offset += 4; + view.setUint32(offset, layer.height, true); offset += 4; - view.setUint32(offset, png.length, true); - offset += 4; - bytes.set(png, offset); - offset += png.length; + view.setUint32(offset, png.length, true); offset += 4; + bytes.set(png, offset); offset += png.length; + + // v3: textData (2-byte length, 0 = no text data) + const tdLen = td ? td.length : 0; + view.setUint16(offset, tdLen, true); offset += 2; + if (td && tdLen > 0) { + bytes.set(td, offset); + offset += tdLen; + } } return new Blob([buffer], {type: WDRAFT_MIME_TYPE}); @@ -141,7 +131,7 @@ export function parseWdraftBinary(buffer: ArrayBuffer): ParsedWdraftFile { // Version const version = bytes[offset++]; - if (version !== WDRAFT_VERSION) { + if (version !== 2 && version !== WDRAFT_VERSION) { throw new Error(`Unsupported .wdraft version: ${version}. Expected ${WDRAFT_VERSION}.`); } @@ -190,7 +180,21 @@ export function parseWdraftBinary(buffer: ArrayBuffer): ParsedWdraftFile { const png = bytes.slice(offset, offset + pngLen); offset += pngLen; - layers.push({id, name, visible, width, height}); + let textData: TextLayerData | undefined; + if (version >= 3) { + const tdLen = view.getUint16(offset, true); + offset += 2; + if (tdLen > 0) { + try { + textData = JSON.parse(dec.decode(bytes.subarray(offset, offset + tdLen))) as TextLayerData; + } catch { + // malformed textData — ignore + } + offset += tdLen; + } + } + + layers.push({id, name, visible, width, height, textData}); pngBuffers.push(png); } diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 70eb888..81fc266 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -22,6 +22,11 @@ export type Size = { export type SizeWithPosition = Size & Point; +export type TextLayerData = { + text: string; + bounds: SizeWithPosition; +}; + export type EditorOptions = { width: number; height: number; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 4ff579e..fb9999a 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -1,4 +1,4 @@ -import type {LayerDocumentSnapshot, LayerSummary} from './layer-manager'; +import type {Layer, LayerDocumentSnapshot, LayerSummary} from './layer-manager'; import { serializeWdraftBinary, WDRAFT_VERSION, @@ -41,6 +41,8 @@ export class WebDraftEditor extends EventTarget { private textBounds: SizeWithPosition | null = null; private textInput: HTMLTextAreaElement | null = null; private skipNextTextPointerDown = false; + private editingTextLayer: Layer | null = null; + private textEditSnapshot: ImageData | null = null; private webPoints: Point[] = []; private clipboard: ClipboardSnapshot | null = null; private pendingHistorySnapshot: HistorySnapshot | null = null; @@ -380,6 +382,7 @@ export class WebDraftEditor extends EventTarget { visible: l.visible, width: l.imageData.width, height: l.imageData.height, + textData: l.textData, })), }; return serializeWdraftBinary(meta, pngBuffers); @@ -400,6 +403,7 @@ export class WebDraftEditor extends EventTarget { visible: l.visible, x: 0, y: 0, + textData: l.textData, imageData: imageDataList[i], })), }; @@ -635,6 +639,15 @@ export class WebDraftEditor extends EventTarget { } this.commitTextInput(); + + const activeLayer = this.layerManager.activeLayer; + if (activeLayer.textData) { + this.enterTextEditMode(activeLayer); + this.isDrawing = false; + this.lastPoint = null; + return; + } + this.textStartPoint = this.lastPoint; this.textBounds = null; this.clearPreview(); @@ -912,6 +925,14 @@ export class WebDraftEditor extends EventTarget { if (event.key === 'Escape') { event.preventDefault(); + const cancelLayer = this.editingTextLayer; + const cancelSnapshot = this.textEditSnapshot; + this.editingTextLayer = null; + this.textEditSnapshot = null; + this.textBounds = null; + if (cancelLayer && cancelSnapshot) { + cancelLayer.context.putImageData(cancelSnapshot, 0, 0); + } this.removeTextInput(); this.clearPreview(); } @@ -933,23 +954,52 @@ export class WebDraftEditor extends EventTarget { const value = this.textInput.value.trimEnd(); const bounds = this.textBounds; + const editingLayer = this.editingTextLayer; + const snapshot = this.textEditSnapshot; this.removeTextInput(); this.clearPreview(); this.skipNextTextPointerDown = options.skipNextPointerDown ?? false; + this.textBounds = null; + this.editingTextLayer = null; + this.textEditSnapshot = null; if (!value.trim()) { - this.textBounds = null; + if (editingLayer && snapshot) { + editingLayer.context.putImageData(snapshot, 0, 0); + } return; } const before = this.captureHistorySnapshot(); - const { context, x: lx, y: ly } = this.layerManager.activeLayer; - const layerBounds = { x: bounds.x - lx, y: bounds.y - ly, width: bounds.width, height: bounds.height }; - drawText(context, value, layerBounds, this.state); + if (editingLayer) { + const { context, x: lx, y: ly } = editingLayer; + drawText(context, value, { x: bounds.x - lx, y: bounds.y - ly, width: bounds.width, height: bounds.height }, this.state); + editingLayer.textData = { text: value, bounds }; + editingLayer.name = `T: ${value.slice(0, 18)}`; + } else { + const newLayer = this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight); + drawText(newLayer.context, value, bounds, this.state); + newLayer.textData = { text: value, bounds }; + newLayer.name = `T: ${value.slice(0, 18)}`; + } + this.pushHistory(before, this.captureHistorySnapshot()); - this.textBounds = null; + this.dispatchChange(); + } + + private enterTextEditMode(layer: Layer): void { + if (!layer.textData) return; + this.textEditSnapshot = layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height); + layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + this.textBounds = layer.textData.bounds; + this.editingTextLayer = layer; + this.createTextInput(layer.textData.bounds); + if (this.textInput) { + this.textInput.value = layer.textData.text; + this.textInput.select(); + } } private removeTextInput(): void { diff --git a/app-v3/src/i18n/en/layers.ts b/app-v3/src/i18n/en/layers.ts index 8b393c1..7eda87f 100644 --- a/app-v3/src/i18n/en/layers.ts +++ b/app-v3/src/i18n/en/layers.ts @@ -11,4 +11,5 @@ export const layersTranslations: LayersTranslations = { renameLayer: 'Rename layer', hideLayer: 'Hide layer', showLayer: 'Show layer', + textLayer: 'Text layer', }; diff --git a/app-v3/src/i18n/pl/layers.ts b/app-v3/src/i18n/pl/layers.ts index 9b8f640..0274f1d 100644 --- a/app-v3/src/i18n/pl/layers.ts +++ b/app-v3/src/i18n/pl/layers.ts @@ -11,4 +11,5 @@ export const layersTranslations: LayersTranslations = { renameLayer: 'Zmień nazwę warstwy', hideLayer: 'Ukryj warstwę', showLayer: 'Pokaż warstwę', + textLayer: 'Warstwa tekstowa', }; diff --git a/app-v3/src/i18n/types.ts b/app-v3/src/i18n/types.ts index b53c33d..753fabf 100644 --- a/app-v3/src/i18n/types.ts +++ b/app-v3/src/i18n/types.ts @@ -108,6 +108,7 @@ export type LayersTranslations = { renameLayer: string; hideLayer: string; showLayer: string; + textLayer: string; }; export type CameraTranslations = { title: string; diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 4cfe03f..861b2f8 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -941,6 +941,7 @@ input[type="number"].cp-field-input::-webkit-inner-spin-button { } .layer-preview { + position: relative; display: grid; place-items: center; padding: 5px; @@ -960,6 +961,20 @@ input[type="number"].cp-field-input::-webkit-inner-spin-button { background-size: 8px 8px; } +.layer-text-badge { + position: absolute; + bottom: 3px; + right: 3px; + font-size: 9px; + font-weight: bold; + line-height: 1; + padding: 1px 3px; + border-radius: 2px; + background: #5b9cff; + color: #fff; + pointer-events: none; +} + .layer-name-input { min-width: 0; padding: 0 10px; diff --git a/app-v3/src/ui/layers-panel.ts b/app-v3/src/ui/layers-panel.ts index 853d858..187d2ee 100644 --- a/app-v3/src/ui/layers-panel.ts +++ b/app-v3/src/ui/layers-panel.ts @@ -66,6 +66,14 @@ export function createLayersPanel(editor: WebDraftEditor): HTMLElement { visibilityButton.textContent = layer.visible ? '●' : '○'; visibilityButton.addEventListener('click', () => editor.toggleLayerVisibility(layer.id)); + if (layer.isTextLayer) { + const badge = document.createElement('span'); + badge.className = 'layer-text-badge'; + badge.textContent = 'T'; + badge.title = t.layers.textLayer; + previewButton.append(badge); + } + item.append(previewButton, renameInput, visibilityButton); return item; From dc9152b451d41c98a64ef479e0322aaf0651d120 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 16:51:04 +0200 Subject: [PATCH 46/62] Defer text layer edit-mode entry to pointerup for improved event handling; add `pendingTextLayerEdit` logic to manage focus race conditions. --- app-v3/src/core/webdraft-editor.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index fb9999a..eff0086 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -41,6 +41,7 @@ export class WebDraftEditor extends EventTarget { private textBounds: SizeWithPosition | null = null; private textInput: HTMLTextAreaElement | null = null; private skipNextTextPointerDown = false; + private pendingTextLayerEdit: Layer | null = null; private editingTextLayer: Layer | null = null; private textEditSnapshot: ImageData | null = null; private webPoints: Point[] = []; @@ -138,6 +139,7 @@ export class WebDraftEditor extends EventTarget { setTool(tool: Tool): void { this.commitTextInput(); + this.pendingTextLayerEdit = null; this.state.activeTool = tool; if (tool !== Tool.Select) { this.clearSelection(); @@ -633,16 +635,20 @@ export class WebDraftEditor extends EventTarget { if (this.skipNextTextPointerDown) { this.skipNextTextPointerDown = false; - this.isDrawing = false; - this.lastPoint = null; - return; + // Don't skip when the click is on a text layer — user wants to re-edit + if (!this.layerManager.activeLayer.textData) { + this.isDrawing = false; + this.lastPoint = null; + return; + } } this.commitTextInput(); const activeLayer = this.layerManager.activeLayer; if (activeLayer.textData) { - this.enterTextEditMode(activeLayer); + // Defer actual edit-mode entry to pointerup to avoid focus race with pointer capture + this.pendingTextLayerEdit = activeLayer; this.isDrawing = false; this.lastPoint = null; return; @@ -724,7 +730,12 @@ export class WebDraftEditor extends EventTarget { } if (this.state.activeTool === Tool.Text) { - this.showTextInput(this.getPoint(event)); + if (this.pendingTextLayerEdit) { + this.enterTextEditMode(this.pendingTextLayerEdit); + this.pendingTextLayerEdit = null; + } else { + this.showTextInput(this.getPoint(event)); + } } if (this.isShapeTool()) { @@ -748,6 +759,7 @@ export class WebDraftEditor extends EventTarget { this.shapeStartPoint = null; this.selectionStartPoint = null; this.textStartPoint = null; + this.pendingTextLayerEdit = null; this.webPoints = []; this.pendingHistorySnapshot = null; this.clearPreview(); From 3fea509e79e07d8aaccc54a3dcf1d2ad377e075c Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 17:36:42 +0200 Subject: [PATCH 47/62] Improve text layer detection and editing: introduce `findTextLayerAt` for identifying text layers at a point, update edit-mode entry logic, and ensure proper active layer synchronization. --- app-v3/src/core/webdraft-editor.ts | 42 +++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index eff0086..ef49005 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -633,10 +633,12 @@ export class WebDraftEditor extends EventTarget { return; } + const textLayerAtPoint = this.findTextLayerAt(this.lastPoint!); + if (this.skipNextTextPointerDown) { this.skipNextTextPointerDown = false; - // Don't skip when the click is on a text layer — user wants to re-edit - if (!this.layerManager.activeLayer.textData) { + // Only skip when there is no text layer to edit at this point + if (!textLayerAtPoint) { this.isDrawing = false; this.lastPoint = null; return; @@ -645,10 +647,8 @@ export class WebDraftEditor extends EventTarget { this.commitTextInput(); - const activeLayer = this.layerManager.activeLayer; - if (activeLayer.textData) { - // Defer actual edit-mode entry to pointerup to avoid focus race with pointer capture - this.pendingTextLayerEdit = activeLayer; + if (textLayerAtPoint) { + this.pendingTextLayerEdit = textLayerAtPoint; this.isDrawing = false; this.lastPoint = null; return; @@ -1003,6 +1003,10 @@ export class WebDraftEditor extends EventTarget { private enterTextEditMode(layer: Layer): void { if (!layer.textData) return; + if (layer.id !== this.layerManager.activeLayer.id) { + this.layerManager.selectLayer(layer.id); + this.dispatchChange(); + } this.textEditSnapshot = layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height); layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); this.textBounds = layer.textData.bounds; @@ -1014,6 +1018,32 @@ export class WebDraftEditor extends EventTarget { } } + private findTextLayerAt(point: Point): Layer | null { + const layers = this.layerManager.allLayers; // ordered bottom to top + for (let i = layers.length - 1; i >= 0; i--) { + const layer = layers[i]; + if (!layer.visible) continue; + + if (layer.textData) { + const b = layer.textData.bounds; + if (point.x >= b.x && point.x < b.x + b.width && + point.y >= b.y && point.y < b.y + b.height) { + return layer; + } + } + + // Check if this layer has a visible pixel at the point — if so, it occludes layers below + const lx = Math.floor(point.x - layer.x); + const ly = Math.floor(point.y - layer.y); + if (lx >= 0 && ly >= 0 && lx < layer.canvas.width && ly < layer.canvas.height) { + if (layer.context.getImageData(lx, ly, 1, 1).data[3] > 10) { + return null; + } + } + } + return null; + } + private removeTextInput(): void { this.textInput?.remove(); this.textInput = null; From 8796bf2ce87d953164ff01bf68e6ce1617cc71ad Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 18:30:50 +0200 Subject: [PATCH 48/62] Add rich text editing support: implement `createTextToolbar`, enhance text styling features, update rendering logic for rich text, and migrate text data to new HTML-based format. --- app-v3/src/core/canvas-drawing.ts | 214 ++++++++++++++++++++++++----- app-v3/src/core/project-file.ts | 33 ++++- app-v3/src/core/types.ts | 10 +- app-v3/src/core/webdraft-editor.ts | 133 ++++++++++-------- app-v3/src/styles.css | 105 +++++++++++++- app-v3/src/ui/text-toolbar.ts | 203 +++++++++++++++++++++++++++ app-v3/src/ui/toolbar-sections.ts | 34 ----- app-v3/src/ui/toolbar.ts | 2 - 8 files changed, 594 insertions(+), 140 deletions(-) create mode 100644 app-v3/src/ui/text-toolbar.ts diff --git a/app-v3/src/core/canvas-drawing.ts b/app-v3/src/core/canvas-drawing.ts index b87a5e0..037bdde 100644 --- a/app-v3/src/core/canvas-drawing.ts +++ b/app-v3/src/core/canvas-drawing.ts @@ -1,45 +1,197 @@ -import type {EditorState, Point, SizeWithPosition} from './types'; +import type {EditorState, Point, SizeWithPosition, TextLayerData} from './types'; import {Tool} from './types'; -export function drawText( +type StyledRun = { + text: string; + bold: boolean; + italic: boolean; + underline: boolean; + color: string; + fontSize: number; + fontFamily: string; +}; + +export function drawRichText( context: CanvasRenderingContext2D, - value: string, - bounds: SizeWithPosition, - state: EditorState, + textData: TextLayerData, + drawBounds: SizeWithPosition, ): void { + const htmlLines = parseHtmlToLines(textData.html, textData); + context.save(); context.globalCompositeOperation = 'source-over'; - context.fillStyle = state.color; - applyShadow(context, state); - context.font = getCanvasFont(state); - context.textAlign = state.textAlign; context.textBaseline = 'top'; - const fontSize = getTextFontSize(state); - const lineHeight = fontSize * 1.25; - const lines = value.split('\n'); - const x = getTextLineX(bounds, state); - - lines.forEach((line, index) => { - const y = bounds.y + index * lineHeight; + let y = drawBounds.y; - if (y + lineHeight <= bounds.y + bounds.height) { - context.fillText(line, x, y, bounds.width); + for (const htmlLine of htmlLines) { + const displayLines = wrapRuns(context, htmlLine, drawBounds.width); + for (const displayLine of displayLines) { + if (y >= drawBounds.y + drawBounds.height) break; + const lineH = maxFontSize(displayLine, textData.defaultFontSize) * 1.4; + if (displayLine.length > 0) drawRichLine(context, displayLine, drawBounds, y, textData.defaultAlign); + y += lineH; } - }); + } context.restore(); } -export function getCanvasFont(state: EditorState): string { - const style = state.textItalic ? 'italic' : 'normal'; - const weight = state.textBold ? '700' : '400'; +function wrapRuns(ctx: CanvasRenderingContext2D, runs: StyledRun[], maxWidth: number): StyledRun[][] { + if (runs.length === 0) return [[]]; + + const displayLines: StyledRun[][] = [[]]; + let lineWidth = 0; + + for (const run of runs) { + const words = run.text.split(/(\s+)/); + for (const word of words) { + if (word === '') continue; + const isSpace = /^\s+$/.test(word); + ctx.font = runToFont(run); + const w = ctx.measureText(word).width; + + if (!isSpace && lineWidth > 0 && lineWidth + w > maxWidth) { + displayLines.push([]); + lineWidth = 0; + } + if (isSpace && lineWidth === 0) continue; + + const currentLine = displayLines[displayLines.length - 1]; + const last = currentLine[currentLine.length - 1]; + if (last && sameStyle(last, run)) { + last.text += word; + } else { + currentLine.push({ ...run, text: word }); + } + lineWidth += w; + } + } + + return displayLines; +} + +function sameStyle(a: StyledRun, b: StyledRun): boolean { + return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && + a.color === b.color && a.fontSize === b.fontSize && a.fontFamily === b.fontFamily; +} + +function maxFontSize(runs: StyledRun[], fallback: number): number { + return runs.length === 0 ? fallback : Math.max(fallback, ...runs.map((r) => r.fontSize)); +} + +function drawRichLine( + ctx: CanvasRenderingContext2D, + runs: StyledRun[], + bounds: SizeWithPosition, + y: number, + align: CanvasTextAlign, +): void { + let totalWidth = 0; + const widths: number[] = []; + for (const run of runs) { + ctx.font = runToFont(run); + const w = ctx.measureText(run.text).width; + widths.push(w); + totalWidth += w; + } + + let x: number; + if (align === 'center') { + x = bounds.x + (bounds.width - totalWidth) / 2; + } else if (align === 'right' || align === 'end') { + x = bounds.x + bounds.width - totalWidth; + } else { + x = bounds.x; + } + + for (let i = 0; i < runs.length; i++) { + const run = runs[i]; + ctx.font = runToFont(run); + ctx.fillStyle = run.color; + ctx.fillText(run.text, x, y); + if (run.underline) { + ctx.save(); + ctx.strokeStyle = run.color; + ctx.lineWidth = Math.max(1, run.fontSize / 16); + ctx.beginPath(); + ctx.moveTo(x, y + run.fontSize + 2); + ctx.lineTo(x + widths[i], y + run.fontSize + 2); + ctx.stroke(); + ctx.restore(); + } + x += widths[i]; + } +} - return `${style} ${weight} ${getTextFontSize(state)}px ${state.textFontFamily}`; +function runToFont(run: StyledRun): string { + return `${run.italic ? 'italic' : 'normal'} ${run.bold ? '700' : '400'} ${run.fontSize}px ${run.fontFamily}`; } -export function getTextFontSize(state: EditorState): number { - return Math.max(8, state.size * 2); +function parseHtmlToLines( + html: string, + defaults: { defaultFontSize: number; defaultFontFamily: string; defaultColor: string }, +): StyledRun[][] { + const container = document.createElement('div'); + container.innerHTML = html; + + const allLines: StyledRun[][] = [[]]; + + const currentLine = (): StyledRun[] => allLines[allLines.length - 1]; + const flush = (): void => { allLines.push([]); }; + + type RunStyle = { bold: boolean; italic: boolean; underline: boolean; color: string; fontSize: number; fontFamily: string }; + + function walk(node: Node, style: RunStyle): void { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? ''; + if (text) currentLine().push({ text, ...style }); + return; + } + if (node.nodeType !== Node.ELEMENT_NODE) return; + + const el = node as HTMLElement; + const tag = el.tagName.toLowerCase(); + + if (tag === 'br') { flush(); return; } + + const isBlock = tag === 'div' || tag === 'p'; + if (isBlock && currentLine().length > 0) flush(); + + const next: RunStyle = { ...style }; + if (tag === 'b' || tag === 'strong') next.bold = true; + if (tag === 'i' || tag === 'em') next.italic = true; + if (tag === 'u') next.underline = true; + + const s = el.style; + if (s) { + if (['bold', '600', '700', '800', '900'].includes(s.fontWeight)) next.bold = true; + if (s.fontStyle === 'italic') next.italic = true; + const td = s.textDecorationLine || s.textDecoration; + if (td?.includes('underline')) next.underline = true; + if (s.color) next.color = s.color; + const px = parseFloat(s.fontSize); + if (px > 0) next.fontSize = px; + if (s.fontFamily) next.fontFamily = s.fontFamily.replace(/['"]/g, '').trim(); + } + + for (const child of Array.from(el.childNodes)) walk(child, next); + + if (isBlock) flush(); + } + + const base: RunStyle = { + bold: false, italic: false, underline: false, + color: defaults.defaultColor, + fontSize: defaults.defaultFontSize, + fontFamily: defaults.defaultFontFamily, + }; + + for (const child of Array.from(container.childNodes)) walk(child, base); + + while (allLines.length > 1 && allLines[allLines.length - 1].length === 0) allLines.pop(); + + return allLines; } export function drawShape( @@ -175,18 +327,6 @@ export function applyShadow(context: CanvasRenderingContext2D, state: EditorStat context.shadowOffsetY = state.shadowOffsetY; } -function getTextLineX(bounds: SizeWithPosition, state: EditorState): number { - if (state.textAlign === 'center') { - return bounds.x + bounds.width / 2; - } - - if (state.textAlign === 'right' || state.textAlign === 'end') { - return bounds.x + bounds.width; - } - - return bounds.x; -} - function hexToRgba(hex: string, alpha: number): string { const normalized = hex.replace('#', ''); const red = Number.parseInt(normalized.slice(0, 2), 16); diff --git a/app-v3/src/core/project-file.ts b/app-v3/src/core/project-file.ts index 4ade4a3..eaa831f 100644 --- a/app-v3/src/core/project-file.ts +++ b/app-v3/src/core/project-file.ts @@ -112,6 +112,37 @@ export function serializeWdraftBinary(meta: WdraftFileMeta, pngBuffers: Uint8Arr return new Blob([buffer], {type: WDRAFT_MIME_TYPE}); } +function migrateTextData(raw: unknown): TextLayerData | undefined { + if (!raw || typeof raw !== 'object') return undefined; + const obj = raw as Record; + + if (typeof obj.html === 'string' && obj.bounds) { + return { + html: obj.html, + bounds: obj.bounds as TextLayerData['bounds'], + defaultFontSize: typeof obj.defaultFontSize === 'number' ? obj.defaultFontSize : 16, + defaultFontFamily: typeof obj.defaultFontFamily === 'string' ? obj.defaultFontFamily : 'sans-serif', + defaultColor: typeof obj.defaultColor === 'string' ? obj.defaultColor : '#000000', + defaultAlign: (typeof obj.defaultAlign === 'string' ? obj.defaultAlign : 'left') as CanvasTextAlign, + }; + } + + // Old format: { text: string, bounds } + if (typeof obj.text === 'string' && obj.bounds) { + const html = obj.text.split('\n').map((l) => l.replace(/&/g, '&').replace(//g, '>')).join('
'); + return { + html, + bounds: obj.bounds as TextLayerData['bounds'], + defaultFontSize: 16, + defaultFontFamily: 'sans-serif', + defaultColor: '#000000', + defaultAlign: 'left', + }; + } + + return undefined; +} + export function parseWdraftBinary(buffer: ArrayBuffer): ParsedWdraftFile { const view = new DataView(buffer); const bytes = new Uint8Array(buffer); @@ -186,7 +217,7 @@ export function parseWdraftBinary(buffer: ArrayBuffer): ParsedWdraftFile { offset += 2; if (tdLen > 0) { try { - textData = JSON.parse(dec.decode(bytes.subarray(offset, offset + tdLen))) as TextLayerData; + textData = migrateTextData(JSON.parse(dec.decode(bytes.subarray(offset, offset + tdLen)))); } catch { // malformed textData — ignore } diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index 81fc266..c820629 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -23,8 +23,12 @@ export type Size = { export type SizeWithPosition = Size & Point; export type TextLayerData = { - text: string; + html: string; bounds: SizeWithPosition; + defaultFontSize: number; + defaultFontFamily: string; + defaultColor: string; + defaultAlign: CanvasTextAlign; }; export type EditorOptions = { @@ -50,8 +54,4 @@ export type EditorState = { shadowBlur: number; shadowOffsetX: number; shadowOffsetY: number; - textFontFamily: string; - textAlign: CanvasTextAlign; - textBold: boolean; - textItalic: boolean; }; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index ef49005..9183a40 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -8,11 +8,12 @@ import { import { drawLine, drawPoint, + drawRichText, drawShape, - drawText, drawWebLine, - getCanvasFont, } from './canvas-drawing'; +import {createTextToolbar} from '../ui/text-toolbar'; +import type {TextToolbarDefaults} from '../ui/text-toolbar'; import { fitNaturalSizeToCanvas, getClippedPasteBounds, @@ -39,7 +40,10 @@ export class WebDraftEditor extends EventTarget { private selectionBounds: SizeWithPosition | null = null; private textStartPoint: Point | null = null; private textBounds: SizeWithPosition | null = null; - private textInput: HTMLTextAreaElement | null = null; + private textInput: HTMLElement | null = null; + private textToolbar: HTMLElement | null = null; + private textToolbarCleanup: (() => void) | null = null; + private textInputDefaults: TextToolbarDefaults | null = null; private skipNextTextPointerDown = false; private pendingTextLayerEdit: Layer | null = null; private editingTextLayer: Layer | null = null; @@ -85,10 +89,6 @@ export class WebDraftEditor extends EventTarget { shadowBlur: 8, shadowOffsetX: 8, shadowOffsetY: 8, - textFontFamily: 'sans-serif', - textAlign: 'left', - textBold: false, - textItalic: false, }; } @@ -207,26 +207,6 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } - setTextFontFamily(fontFamily: string): void { - this.state.textFontFamily = fontFamily; - this.dispatchChange(); - } - - setTextAlign(align: CanvasTextAlign): void { - this.state.textAlign = align; - this.dispatchChange(); - } - - setTextBold(enabled: boolean): void { - this.state.textBold = enabled; - this.dispatchChange(); - } - - setTextItalic(enabled: boolean): void { - this.state.textItalic = enabled; - this.dispatchChange(); - } - resizeCanvas(width: number, height: number, resizeLayersToo = true): void { this.commitTextInput(); this.clearSelection(); @@ -901,40 +881,40 @@ export class WebDraftEditor extends EventTarget { } private showTextInput(point: Point): void { - if (!this.textStartPoint) { - return; - } + if (!this.textStartPoint) return; const bounds = normalizeTextBounds(getBounds(this.textStartPoint, point), this.canvasSize); - this.textBounds = bounds; this.clearPreview(); - this.createTextInput(bounds); + this.createTextInput(bounds, { + fontSize: 16, + fontFamily: 'sans-serif', + color: this.state.color, + align: 'left', + }); this.textStartPoint = null; } - private createTextInput(bounds: SizeWithPosition): void { + private createTextInput(bounds: SizeWithPosition, defaults: TextToolbarDefaults): void { this.removeTextInput(); - const input = document.createElement('textarea'); + const input = document.createElement('div'); input.className = 'text-input-layer'; + input.contentEditable = 'true'; input.style.left = `${bounds.x}px`; input.style.top = `${bounds.y}px`; input.style.width = `${bounds.width}px`; input.style.height = `${bounds.height}px`; - input.style.color = this.state.color; - input.style.font = getCanvasFont(this.state); - input.style.fontWeight = this.state.textBold ? '700' : '400'; - input.style.fontStyle = this.state.textItalic ? 'italic' : 'normal'; - input.style.textAlign = this.state.textAlign; - input.placeholder = 'Text'; + input.style.color = defaults.color; + input.style.fontSize = `${defaults.fontSize}px`; + input.style.fontFamily = defaults.fontFamily; + input.style.textAlign = defaults.align; input.addEventListener('keydown', (event) => { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); this.commitTextInput(); } - if (event.key === 'Escape') { event.preventDefault(); const cancelLayer = this.editingTextLayer; @@ -950,24 +930,34 @@ export class WebDraftEditor extends EventTarget { } }); - input.addEventListener('blur', () => { + input.addEventListener('blur', (event) => { + const related = (event as FocusEvent).relatedTarget as Node | null; + if (this.textToolbar?.contains(related)) return; this.commitTextInput({skipNextPointerDown: true}); }); this.textInput = input; + this.textInputDefaults = defaults; + + const {element: toolbar, cleanup} = createTextToolbar(input, bounds, defaults, (align) => { + if (this.textInputDefaults) this.textInputDefaults.align = align; + }); + this.textToolbar = toolbar; + this.textToolbarCleanup = cleanup; + this.root.append(input); + this.root.append(toolbar); input.focus(); } private commitTextInput(options: {skipNextPointerDown?: boolean} = {}): void { - if (!this.textInput || !this.textBounds) { - return; - } + if (!this.textInput || !this.textBounds) return; - const value = this.textInput.value.trimEnd(); + const html = this.textInput.innerHTML; const bounds = this.textBounds; const editingLayer = this.editingTextLayer; const snapshot = this.textEditSnapshot; + const defaults = this.textInputDefaults ?? {fontSize: 16, fontFamily: 'sans-serif', color: '#000000', align: 'left' as CanvasTextAlign}; this.removeTextInput(); this.clearPreview(); @@ -976,25 +966,33 @@ export class WebDraftEditor extends EventTarget { this.editingTextLayer = null; this.textEditSnapshot = null; - if (!value.trim()) { - if (editingLayer && snapshot) { - editingLayer.context.putImageData(snapshot, 0, 0); - } + const plainText = htmlToPlainText(html); + if (!plainText.trim()) { + if (editingLayer && snapshot) editingLayer.context.putImageData(snapshot, 0, 0); return; } + const textData = { + html, + bounds, + defaultFontSize: defaults.fontSize, + defaultFontFamily: defaults.fontFamily, + defaultColor: defaults.color, + defaultAlign: defaults.align, + }; + const before = this.captureHistorySnapshot(); if (editingLayer) { - const { context, x: lx, y: ly } = editingLayer; - drawText(context, value, { x: bounds.x - lx, y: bounds.y - ly, width: bounds.width, height: bounds.height }, this.state); - editingLayer.textData = { text: value, bounds }; - editingLayer.name = `T: ${value.slice(0, 18)}`; + const {context, x: lx, y: ly} = editingLayer; + drawRichText(context, textData, {x: bounds.x - lx, y: bounds.y - ly, width: bounds.width, height: bounds.height}); + editingLayer.textData = textData; + editingLayer.name = `T: ${plainText.slice(0, 18)}`; } else { const newLayer = this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight); - drawText(newLayer.context, value, bounds, this.state); - newLayer.textData = { text: value, bounds }; - newLayer.name = `T: ${value.slice(0, 18)}`; + drawRichText(newLayer.context, textData, bounds); + newLayer.textData = textData; + newLayer.name = `T: ${plainText.slice(0, 18)}`; } this.pushHistory(before, this.captureHistorySnapshot()); @@ -1011,10 +1009,14 @@ export class WebDraftEditor extends EventTarget { layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height); this.textBounds = layer.textData.bounds; this.editingTextLayer = layer; - this.createTextInput(layer.textData.bounds); + this.createTextInput(layer.textData.bounds, { + fontSize: layer.textData.defaultFontSize, + fontFamily: layer.textData.defaultFontFamily, + color: layer.textData.defaultColor, + align: layer.textData.defaultAlign, + }); if (this.textInput) { - this.textInput.value = layer.textData.text; - this.textInput.select(); + this.textInput.innerHTML = layer.textData.html; } } @@ -1045,8 +1047,13 @@ export class WebDraftEditor extends EventTarget { } private removeTextInput(): void { + this.textToolbarCleanup?.(); + this.textToolbarCleanup = null; + this.textToolbar?.remove(); + this.textToolbar = null; this.textInput?.remove(); this.textInput = null; + this.textInputDefaults = null; } private renderTextFrame(): void { @@ -1176,6 +1183,12 @@ function createImageData(buffer: {data: Uint8ClampedArray; width: number; height return new ImageData(data, buffer.width, buffer.height); } +function htmlToPlainText(html: string): string { + const tmp = document.createElement('div'); + tmp.innerHTML = html; + return tmp.textContent ?? ''; +} + function loadPngBytesAsImageData(bytes: Uint8Array, width: number, height: number): Promise { return new Promise((resolve, reject) => { const url = URL.createObjectURL(new Blob([(bytes.buffer as ArrayBuffer).slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], {type: 'image/png'})); diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 861b2f8..698518a 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -245,12 +245,115 @@ select { min-width: 120px; min-height: 40px; padding: 6px; - resize: none; border: 1px solid #5b9cff; border-radius: 4px; outline: 2px solid rgb(91 156 255 / 20%); background: rgb(255 255 255 / 82%); caret-color: #111111; + line-height: 1.4; + word-break: break-word; + overflow-wrap: break-word; + white-space: pre-wrap; + overflow: hidden; +} + +.text-input-layer:focus { + outline: 2px solid rgb(91 156 255 / 40%); +} + +.text-toolbar { + position: absolute; + z-index: 30; + display: flex; + align-items: center; + gap: 2px; + padding: 4px 6px; + background: #1e2329; + border: 1px solid #3a4049; + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.55); + user-select: none; + height: 36px; +} + +.text-toolbar-group { + display: flex; + align-items: center; + gap: 2px; +} + +.text-toolbar-sep { + width: 1px; + height: 20px; + background: #3a4049; + margin: 0 4px; + flex-shrink: 0; +} + +.text-toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + min-width: 26px; + height: 26px; + padding: 0 5px; + border: 1px solid transparent; + border-radius: 4px; + background: none; + color: #c9d1dc; + font-size: 13px; + cursor: pointer; + line-height: 1; +} + +.text-toolbar-btn:hover { + background: #2d3340; + border-color: #3a4049; +} + +.text-toolbar-btn.is-active { + background: #1e3a5c; + border-color: #4b89ff; + color: #fff; +} + +.text-toolbar-select { + height: 26px; + padding: 0 4px; + border: 1px solid #3a4049; + border-radius: 4px; + background: #252b33; + color: #c9d1dc; + font-size: 12px; + cursor: pointer; +} + +.text-toolbar-size-select { + width: 56px; +} + +.text-toolbar-color { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: 1px solid #3a4049; + border-radius: 4px; + background: #252b33; + cursor: pointer; + overflow: hidden; + padding: 2px; +} + +.text-toolbar-color input[type="color"] { + width: 20px; + height: 20px; + padding: 0; + border: none; + border-radius: 2px; + cursor: pointer; + background: none; } .toolbar { diff --git a/app-v3/src/ui/text-toolbar.ts b/app-v3/src/ui/text-toolbar.ts new file mode 100644 index 0000000..c7151b6 --- /dev/null +++ b/app-v3/src/ui/text-toolbar.ts @@ -0,0 +1,203 @@ +import type { SizeWithPosition } from '../core/types'; + +export type TextToolbarDefaults = { + fontSize: number; + fontFamily: string; + color: string; + align: CanvasTextAlign; +}; + +const FONT_FAMILIES = ['sans-serif', 'serif', 'monospace', 'cursive', 'fantasy']; +const FONT_SIZES = [8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 64, 72, 96, 128]; + +const ALIGN_ICONS: Record = { + left: '', + center: '', + right: '', +}; + +export function createTextToolbar( + contentEl: HTMLElement, + bounds: SizeWithPosition, + defaults: TextToolbarDefaults, + onAlignChange: (align: CanvasTextAlign) => void, +): { element: HTMLElement; cleanup: () => void } { + const toolbar = document.createElement('div'); + toolbar.className = 'text-toolbar'; + + const top = bounds.y >= 44 ? bounds.y - 44 : bounds.y + bounds.height + 8; + toolbar.style.top = `${top}px`; + toolbar.style.left = `${Math.max(0, bounds.x)}px`; + + // --- helpers --- + + const mkBtn = (title: string, html: string, extraClass = ''): HTMLButtonElement => { + const b = document.createElement('button'); + b.type = 'button'; + b.title = title; + b.className = `text-toolbar-btn${extraClass ? ' ' + extraClass : ''}`; + b.innerHTML = html; + b.addEventListener('mousedown', (e) => e.preventDefault()); + return b; + }; + + const sep = (): HTMLElement => { + const d = document.createElement('div'); + d.className = 'text-toolbar-sep'; + return d; + }; + + let savedRange: Range | null = null; + + const saveRange = (): void => { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0) { + const r = sel.getRangeAt(0); + if (contentEl.contains(r.commonAncestorContainer)) savedRange = r.cloneRange(); + } + }; + + const restoreAndApply = (fn: () => void): void => { + contentEl.focus(); + if (savedRange) { + const sel = window.getSelection(); + if (sel) { sel.removeAllRanges(); sel.addRange(savedRange); } + savedRange = null; + } + fn(); + }; + + // --- Alignment --- + + const alignBtns: Record = { + left: mkBtn('Align left', ALIGN_ICONS['left'], 'text-toolbar-align'), + center: mkBtn('Align center', ALIGN_ICONS['center'], 'text-toolbar-align'), + right: mkBtn('Align right', ALIGN_ICONS['right'], 'text-toolbar-align'), + }; + + const setAlign = (align: CanvasTextAlign): void => { + Object.values(alignBtns).forEach((b) => b.classList.remove('is-active')); + const key = align === 'start' ? 'left' : align === 'end' ? 'right' : align; + alignBtns[key]?.classList.add('is-active'); + defaults.align = align; + contentEl.style.textAlign = align; + onAlignChange(align); + }; + + Object.entries(alignBtns).forEach(([align, btn]) => { + btn.addEventListener('click', () => setAlign(align as CanvasTextAlign)); + }); + setAlign(defaults.align); + + const alignGroup = document.createElement('div'); + alignGroup.className = 'text-toolbar-group'; + alignGroup.append(...Object.values(alignBtns)); + + // --- Bold / Italic / Underline --- + + const btnBold = mkBtn('Bold', 'B', 'text-toolbar-format'); + const btnItalic = mkBtn('Italic', 'I', 'text-toolbar-format'); + const btnUnderline = mkBtn('Underline', 'U', 'text-toolbar-format'); + + btnBold.addEventListener('click', () => document.execCommand('bold')); + btnItalic.addEventListener('click', () => document.execCommand('italic')); + btnUnderline.addEventListener('click', () => document.execCommand('underline')); + + const formatGroup = document.createElement('div'); + formatGroup.className = 'text-toolbar-group'; + formatGroup.append(btnBold, btnItalic, btnUnderline); + + // --- Font family --- + + const fontFamilySelect = document.createElement('select'); + fontFamilySelect.className = 'text-toolbar-select'; + fontFamilySelect.title = 'Font family'; + FONT_FAMILIES.forEach((f) => { + const o = document.createElement('option'); + o.value = f; o.textContent = f; + if (f === defaults.fontFamily) o.selected = true; + fontFamilySelect.append(o); + }); + fontFamilySelect.addEventListener('mousedown', saveRange); + fontFamilySelect.addEventListener('change', () => { + restoreAndApply(() => applyInlineStyle(contentEl, 'font-family', fontFamilySelect.value)); + }); + + // --- Font size --- + + const fontSizeSelect = document.createElement('select'); + fontSizeSelect.className = 'text-toolbar-select text-toolbar-size-select'; + fontSizeSelect.title = 'Font size'; + FONT_SIZES.forEach((s) => { + const o = document.createElement('option'); + o.value = `${s}`; o.textContent = `${s}`; + if (s === defaults.fontSize) o.selected = true; + fontSizeSelect.append(o); + }); + fontSizeSelect.addEventListener('mousedown', saveRange); + fontSizeSelect.addEventListener('change', () => { + const px = parseInt(fontSizeSelect.value); + if (px > 0) { + defaults.fontSize = px; + restoreAndApply(() => applyInlineStyle(contentEl, 'font-size', `${px}px`)); + } + }); + + const fontGroup = document.createElement('div'); + fontGroup.className = 'text-toolbar-group'; + fontGroup.append(fontFamilySelect, fontSizeSelect); + + // --- Color --- + + const colorLabel = document.createElement('label'); + colorLabel.className = 'text-toolbar-color'; + colorLabel.title = 'Text color'; + const colorInput = document.createElement('input'); + colorInput.type = 'color'; + colorInput.value = defaults.color; + colorLabel.append(colorInput); + + colorInput.addEventListener('mousedown', saveRange); + colorInput.addEventListener('change', () => { + restoreAndApply(() => applyInlineStyle(contentEl, 'color', colorInput.value)); + }); + + toolbar.append(alignGroup, sep(), formatGroup, sep(), fontGroup, sep(), colorLabel); + + // --- Selection state update --- + + const onSelectionChange = (): void => { + if (!contentEl.contains(window.getSelection()?.anchorNode ?? null)) return; + btnBold.classList.toggle('is-active', document.queryCommandState('bold')); + btnItalic.classList.toggle('is-active', document.queryCommandState('italic')); + btnUnderline.classList.toggle('is-active', document.queryCommandState('underline')); + }; + document.addEventListener('selectionchange', onSelectionChange); + + return { + element: toolbar, + cleanup: () => document.removeEventListener('selectionchange', onSelectionChange), + }; +} + +function applyInlineStyle(contentEl: HTMLElement, prop: string, value: string): void { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return; + const range = sel.getRangeAt(0); + if (!contentEl.contains(range.commonAncestorContainer)) return; + if (sel.isCollapsed) return; + + try { + const fragment = range.extractContents(); + const span = document.createElement('span'); + span.style.setProperty(prop, value); + span.appendChild(fragment); + range.insertNode(span); + const newRange = document.createRange(); + newRange.selectNodeContents(span); + sel.removeAllRanges(); + sel.addRange(newRange); + } catch { + // Cross-block selections may fail — ignore + } +} diff --git a/app-v3/src/ui/toolbar-sections.ts b/app-v3/src/ui/toolbar-sections.ts index 9712f29..6ae51a5 100644 --- a/app-v3/src/ui/toolbar-sections.ts +++ b/app-v3/src/ui/toolbar-sections.ts @@ -312,40 +312,6 @@ export function createShadowSection(editor: WebDraftEditor): ToolbarSection { }; } -export function createTextSection(editor: WebDraftEditor): ToolbarSection { - const fontSelect = createSelectControl(t.toolbar.textFont, ['sans-serif', 'serif', 'monospace', 'cursive'], (font) => { - editor.setTextFontFamily(font); - }); - - const alignSelect = createSelectControl(t.toolbar.textAlignment, ['left', 'center', 'right'] as const, (align) => { - editor.setTextAlign(align); - }); - - const textBoldToggle = createCheckboxControl(t.toolbar.bold, editor.state.textBold, (enabled) => { - editor.setTextBold(enabled); - }); - const textItalicToggle = createCheckboxControl(t.toolbar.italic, editor.state.textItalic, (enabled) => { - editor.setTextItalic(enabled); - }); - - const element = createToolbarSection( - createGrid('field-grid', fontSelect, alignSelect, textBoldToggle.element, textItalicToggle.element), - ); - - element.hidden = editor.state.activeTool !== Tool.Text; - - return { - element, - sync: () => { - element.hidden = editor.state.activeTool !== Tool.Text; - fontSelect.value = editor.state.textFontFamily; - alignSelect.value = editor.state.textAlign; - textBoldToggle.setChecked(editor.state.textBold); - textItalicToggle.setChecked(editor.state.textItalic); - }, - }; -} - export function createEditSection(editor: WebDraftEditor): ToolbarSection { const clearButton = createEditButton( t.toolbar.clear, diff --git a/app-v3/src/ui/toolbar.ts b/app-v3/src/ui/toolbar.ts index 557bd92..37bcc93 100644 --- a/app-v3/src/ui/toolbar.ts +++ b/app-v3/src/ui/toolbar.ts @@ -9,7 +9,6 @@ import { createResizeSection, createShadowSection, createStyleSection, - createTextSection, createToolSection, syncToolbarSections, } from './toolbar-sections'; @@ -42,7 +41,6 @@ export function createToolbar(editor: WebDraftEditor, status: StatusReporter): H createToolSection(editor), createStyleSection(editor), createShadowSection(editor), - createTextSection(editor), createResizeSection(editor, status), createFileSection(editor, fileInput, exportImage, status), ]; From 8739eec9ee3dce6917aa024aaee2818e002bbaa3 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 18:40:56 +0200 Subject: [PATCH 49/62] Add canvas zoom support: implement zoom levels, keyboard shortcuts, mouse wheel handling, UI widget, and update rendering logic for zoom synchronization. --- app-v3/src/core/types.ts | 1 + app-v3/src/core/webdraft-editor.ts | 45 ++++++++++++++++++++++-- app-v3/src/styles.css | 53 +++++++++++++++++++++++++++-- app-v3/src/ui/keyboard-shortcuts.ts | 18 ++++++++++ app-v3/src/ui/menu-bar.ts | 41 +++++++++++++++++++++- 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/app-v3/src/core/types.ts b/app-v3/src/core/types.ts index c820629..4a69256 100644 --- a/app-v3/src/core/types.ts +++ b/app-v3/src/core/types.ts @@ -40,6 +40,7 @@ export type EditorOptions = { export type EditorState = { activeTool: Tool; + zoom: number; color: string; fillColor: string; fillEnabled: boolean; diff --git a/app-v3/src/core/webdraft-editor.ts b/app-v3/src/core/webdraft-editor.ts index 9183a40..a114f1a 100644 --- a/app-v3/src/core/webdraft-editor.ts +++ b/app-v3/src/core/webdraft-editor.ts @@ -27,6 +27,8 @@ import {LayerManager} from './layer-manager'; import {invertPixelBuffer, mirrorPixelBuffer, rotatePixelBuffer} from './layer-transforms'; import {EditorOptions, EditorState, Point, SizeWithPosition, Tool} from './types'; +const ZOOM_STEPS = [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4, 6, 8]; + export class WebDraftEditor extends EventTarget { private readonly root: HTMLElement; private readonly layerManager: LayerManager; @@ -75,6 +77,7 @@ export class WebDraftEditor extends EventTarget { this.state = { activeTool: Tool.Pencil, + zoom: 1, color: options.color, fillColor: '#ffffff', fillEnabled: false, @@ -207,6 +210,22 @@ export class WebDraftEditor extends EventTarget { this.dispatchChange(); } + setZoom(zoom: number): void { + this.state.zoom = Math.min(Math.max(zoom, 0.1), 8); + this.applyZoom(); + this.dispatchChange(); + } + + zoomIn(): void { + const next = ZOOM_STEPS.find((z) => z > this.state.zoom + 0.001); + if (next !== undefined) this.setZoom(next); + } + + zoomOut(): void { + const prev = [...ZOOM_STEPS].reverse().find((z) => z < this.state.zoom - 0.001); + if (prev !== undefined) this.setZoom(prev); + } + resizeCanvas(width: number, height: number, resizeLayersToo = true): void { this.commitTextInput(); this.clearSelection(); @@ -744,6 +763,14 @@ export class WebDraftEditor extends EventTarget { this.pendingHistorySnapshot = null; this.clearPreview(); }); + + this.eventLayer.addEventListener('wheel', (event) => { + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + if (event.deltaY < 0) this.zoomIn(); + else this.zoomOut(); + } + }, {passive: false}); } private toLayerPoint(p: Point): Point { @@ -753,10 +780,10 @@ export class WebDraftEditor extends EventTarget { private getPoint(event: PointerEvent): Point { const rect = this.eventLayer.getBoundingClientRect(); - + const zoom = this.state.zoom; return { - x: event.clientX - rect.left, - y: event.clientY - rect.top, + x: (event.clientX - rect.left) / zoom, + y: (event.clientY - rect.top) / zoom, }; } @@ -782,6 +809,18 @@ export class WebDraftEditor extends EventTarget { this.root.style.setProperty('--canvas-height', `${this.state.canvasHeight}px`); this.previewCanvas.width = this.state.canvasWidth; this.previewCanvas.height = this.state.canvasHeight; + this.applyZoom(); + } + + private applyZoom(): void { + const zoom = this.state.zoom; + const w = this.state.canvasWidth; + const h = this.state.canvasHeight; + this.root.style.transform = zoom !== 1 ? `scale(${zoom})` : ''; + this.root.style.transformOrigin = 'top left'; + // Expand layout area so workspace scroll reflects the visual size + this.root.style.marginRight = zoom > 1 ? `${Math.round(w * (zoom - 1))}px` : ''; + this.root.style.marginBottom = zoom > 1 ? `${Math.round(h * (zoom - 1))}px` : ''; } private get canvasSize(): {width: number; height: number} { diff --git a/app-v3/src/styles.css b/app-v3/src/styles.css index 698518a..8f4e70e 100644 --- a/app-v3/src/styles.css +++ b/app-v3/src/styles.css @@ -69,7 +69,6 @@ select { } .menu-lang-selector { - margin-left: auto; display: flex; align-items: stretch; } @@ -88,6 +87,55 @@ select { background: #2e6fb7; } +/* ── Zoom widget ─────────────────────────────────────────── */ + +.menu-zoom { + display: flex; + align-items: center; + gap: 1px; + margin-left: auto; + padding: 0 6px; +} + +.menu-zoom-btn, +.menu-zoom-label { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + padding: 0 6px; + border: 1px solid transparent; + border-radius: 3px; + background: none; + color: #c8cdd4; + font-size: 12px; + cursor: pointer; + line-height: 1; + white-space: nowrap; +} + +.menu-zoom-btn { + min-width: 20px; + font-size: 15px; +} + +.menu-zoom-btn:hover, +.menu-zoom-label:hover { + background: #2c3038; + border-color: #3a4049; + color: #f4f7fb; +} + +.menu-zoom-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.menu-zoom-btn:disabled:hover { + background: none; + border-color: transparent; +} + .menu-btn { display: flex; align-items: center; @@ -191,10 +239,9 @@ select { .canvas-surface { position: relative; + flex-shrink: 0; width: var(--canvas-width); height: var(--canvas-height); - max-width: 100%; - max-height: calc(100vh - 64px); background-color: #f9fafb; background-image: linear-gradient(45deg, #d6dae0 25%, transparent 25%), diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/app-v3/src/ui/keyboard-shortcuts.ts index de76e00..1263c9f 100644 --- a/app-v3/src/ui/keyboard-shortcuts.ts +++ b/app-v3/src/ui/keyboard-shortcuts.ts @@ -44,6 +44,24 @@ export function bindKeyboardShortcuts(editor: WebDraftEditor, options: ShortcutO return; } + if (isCommand && (key === '=' || key === '+')) { + event.preventDefault(); + editor.zoomIn(); + return; + } + + if (isCommand && key === '-') { + event.preventDefault(); + editor.zoomOut(); + return; + } + + if (isCommand && key === '0') { + event.preventDefault(); + editor.setZoom(1); + return; + } + if (isCommand && key === 'c') { event.preventDefault(); editor.copySelection(); diff --git a/app-v3/src/ui/menu-bar.ts b/app-v3/src/ui/menu-bar.ts index ee0024f..5c86504 100644 --- a/app-v3/src/ui/menu-bar.ts +++ b/app-v3/src/ui/menu-bar.ts @@ -451,7 +451,7 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H bar.append(createMenuButton(menu)); } - bar.append(createLangSelector(editor)); + bar.append(createZoomWidget(editor), createLangSelector(editor)); document.addEventListener('click', closeOpenMenu); document.addEventListener('keydown', (e) => { @@ -460,3 +460,42 @@ export function createMenuBar(editor: WebDraftEditor, status: StatusReporter): H return bar; } + +function createZoomWidget(editor: WebDraftEditor): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = 'menu-zoom'; + + const btnOut = document.createElement('button'); + btnOut.type = 'button'; + btnOut.className = 'menu-zoom-btn'; + btnOut.textContent = '−'; + btnOut.title = 'Zoom out (Ctrl+−)'; + btnOut.addEventListener('click', () => editor.zoomOut()); + + const label = document.createElement('button'); + label.type = 'button'; + label.className = 'menu-zoom-label'; + label.title = 'Reset zoom (Ctrl+0)'; + label.addEventListener('click', () => editor.setZoom(1)); + + const btnIn = document.createElement('button'); + btnIn.type = 'button'; + btnIn.className = 'menu-zoom-btn'; + btnIn.textContent = '+'; + btnIn.title = 'Zoom in (Ctrl+=)'; + btnIn.addEventListener('click', () => editor.zoomIn()); + + wrap.append(btnOut, label, btnIn); + + const sync = (): void => { + const pct = Math.round(editor.state.zoom * 100); + label.textContent = `${pct}%`; + btnOut.disabled = editor.state.zoom <= 0.1; + btnIn.disabled = editor.state.zoom >= 8; + }; + + editor.addEventListener('change', sync); + sync(); + + return wrap; +} From abfdc2278f1b4e997b1143d77348a905a807ce1b Mon Sep 17 00:00:00 2001 From: rtcoder Date: Thu, 11 Jun 2026 18:45:50 +0200 Subject: [PATCH 50/62] Remove unused core modules: delete `canvas-drawing`, `editor-geometry`, `flood-fill`, `history-manager`, `i18n`, `layer-manager`, `layer-transforms`, and `project-file`. --- .gitignore | 5 +- .gitmodules | 3 - Gruntfile.js | 54 - app-v3/.gitignore | 2 - app-v3/index.html | 12 - app-v3/package-lock.json | 1002 ------ app-v3/package.json | 17 - build/css/style.css | 1025 ------ build/css/style.css.map | 1 - build/css/style.min.css | 1 - build/js/built.js | 2049 ------------ index.html | 88 +- js/modules/Camera.js | 171 - js/modules/Clipboard.js | 0 js/modules/Contextmenu.js | 89 - js/modules/Draw.js | 64 - js/modules/File.js | 67 - js/modules/Layers.js | 349 -- js/modules/Resizer.js | 51 - js/modules/Select.js | 132 - js/modules/Shapes.js | 162 - js/modules/Text.js | 135 - js/modules/WebDraft.js | 381 --- js/modules/events.module.js | 134 - js/modules/keys.module.js | 171 - js/start.js | 141 - less/camera.less | 181 -- less/contextmenu.less | 118 - less/hints.less | 43 - less/info.less | 63 - less/inputs.less | 149 - less/layers.less | 110 - less/resizer.less | 50 - less/shadow-changer.less | 41 - less/sliders.less | 31 - less/statusbar.less | 32 - less/style.less | 92 - less/text-options.less | 53 - less/tools.less | 150 - less/variables.less | 31 - package-lock.json | 2465 ++++++-------- package.json | 33 +- parts/buttons.part.html | 42 - parts/camera.part.html | 38 - parts/color-picker.part.html | 15 - parts/info.part.html | 61 - parts/layers.part.html | 11 - parts/resizer.part.html | 7 - parts/sliders.part.html | 18 - parts/text-options.part.html | 36 - pic/transparent.png | Bin 195 -> 0 bytes {app-v3/src => src}/core/canvas-drawing.ts | 0 {app-v3/src => src}/core/editor-geometry.ts | 0 {app-v3/src => src}/core/flood-fill.ts | 0 {app-v3/src => src}/core/history-manager.ts | 0 {app-v3/src => src}/core/i18n.ts | 0 {app-v3/src => src}/core/layer-manager.ts | 0 {app-v3/src => src}/core/layer-transforms.ts | 0 {app-v3/src => src}/core/project-file.ts | 0 {app-v3/src => src}/core/types.ts | 0 {app-v3/src => src}/core/webdraft-editor.ts | 0 {app-v3/src => src}/i18n/en/camera.ts | 0 {app-v3/src => src}/i18n/en/common.ts | 0 {app-v3/src => src}/i18n/en/edit.ts | 0 {app-v3/src => src}/i18n/en/file.ts | 0 {app-v3/src => src}/i18n/en/image.ts | 0 {app-v3/src => src}/i18n/en/index.ts | 0 {app-v3/src => src}/i18n/en/layer.ts | 0 {app-v3/src => src}/i18n/en/layers.ts | 0 {app-v3/src => src}/i18n/en/menu.ts | 0 {app-v3/src => src}/i18n/en/picker.ts | 0 {app-v3/src => src}/i18n/en/saveDialog.ts | 0 {app-v3/src => src}/i18n/en/toolbar.ts | 0 {app-v3/src => src}/i18n/pl/camera.ts | 0 {app-v3/src => src}/i18n/pl/common.ts | 0 {app-v3/src => src}/i18n/pl/edit.ts | 0 {app-v3/src => src}/i18n/pl/file.ts | 0 {app-v3/src => src}/i18n/pl/image.ts | 0 {app-v3/src => src}/i18n/pl/index.ts | 0 {app-v3/src => src}/i18n/pl/layer.ts | 0 {app-v3/src => src}/i18n/pl/layers.ts | 0 {app-v3/src => src}/i18n/pl/menu.ts | 0 {app-v3/src => src}/i18n/pl/picker.ts | 0 {app-v3/src => src}/i18n/pl/saveDialog.ts | 0 {app-v3/src => src}/i18n/pl/toolbar.ts | 0 {app-v3/src => src}/i18n/types.ts | 0 {app-v3/src => src}/main.ts | 0 {app-v3/src => src}/styles.css | 0 {app-v3/src => src}/ui/camera-panel.ts | 0 {app-v3/src => src}/ui/color-picker.ts | 0 {app-v3/src => src}/ui/keyboard-shortcuts.ts | 0 {app-v3/src => src}/ui/lang-state.ts | 0 {app-v3/src => src}/ui/layers-panel.ts | 0 {app-v3/src => src}/ui/menu-bar.ts | 0 {app-v3/src => src}/ui/resize-dialog.ts | 0 {app-v3/src => src}/ui/right-panel.ts | 0 {app-v3/src => src}/ui/save-dialog.ts | 0 {app-v3/src => src}/ui/status-toasts.ts | 0 {app-v3/src => src}/ui/text-toolbar.ts | 0 {app-v3/src => src}/ui/toolbar-controls.ts | 0 {app-v3/src => src}/ui/toolbar-sections.ts | 0 {app-v3/src => src}/ui/toolbar.ts | 0 .../tests => tests}/editor-geometry.test.mjs | 0 {app-v3/tests => tests}/flood-fill.test.mjs | 0 .../tests => tests}/history-manager.test.mjs | 0 .../tests => tests}/layer-transforms.test.mjs | 0 {app-v3/tests => tests}/test-support.mjs | 0 app-v3/tsconfig.json => tsconfig.json | 0 vendor/canvas-to-blob/canvas-toBlob.js | 124 - vendor/file-saver/FileSaver.min.js | 2 - vendor/fontawesome/css/fa-brands.css | 13 - vendor/fontawesome/css/fa-brands.min.css | 5 - vendor/fontawesome/css/fa-regular.css | 14 - vendor/fontawesome/css/fa-regular.min.css | 5 - vendor/fontawesome/css/fa-solid.css | 15 - vendor/fontawesome/css/fa-solid.min.css | 5 - vendor/fontawesome/css/fontawesome-all.css | 2726 ---------------- .../fontawesome/css/fontawesome-all.min.css | 5 - vendor/fontawesome/css/fontawesome.css | 2696 ---------------- vendor/fontawesome/css/fontawesome.min.css | 5 - vendor/fontawesome/webfonts/fa-brands-400.eot | Bin 98640 -> 0 bytes vendor/fontawesome/webfonts/fa-brands-400.svg | 1008 ------ vendor/fontawesome/webfonts/fa-brands-400.ttf | Bin 98404 -> 0 bytes .../fontawesome/webfonts/fa-brands-400.woff | Bin 63728 -> 0 bytes .../fontawesome/webfonts/fa-brands-400.woff2 | Bin 54468 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.eot | Bin 31156 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.svg | 366 --- .../fontawesome/webfonts/fa-regular-400.ttf | Bin 30928 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.woff | Bin 14712 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.woff2 | Bin 12220 -> 0 bytes vendor/fontawesome/webfonts/fa-solid-900.eot | Bin 106004 -> 0 bytes vendor/fontawesome/webfonts/fa-solid-900.svg | 1518 --------- vendor/fontawesome/webfonts/fa-solid-900.ttf | Bin 105784 -> 0 bytes vendor/fontawesome/webfonts/fa-solid-900.woff | Bin 50524 -> 0 bytes .../fontawesome/webfonts/fa-solid-900.woff2 | Bin 40244 -> 0 bytes vendor/html2canvas/html2canvas.js | 2868 ----------------- vendor/jquery-ui/css/jquery-ui-1.12.1.min.css | 7 - vendor/jquery-ui/js/jquery-ui-1.12.1.min.js | 13 - vendor/jquery/jquery-3.2.1.min.js | 4 - vendor/rtcoder/colorpicker | 1 - 140 files changed, 983 insertions(+), 20593 deletions(-) delete mode 100644 .gitmodules delete mode 100644 Gruntfile.js delete mode 100644 app-v3/.gitignore delete mode 100644 app-v3/index.html delete mode 100644 app-v3/package-lock.json delete mode 100644 app-v3/package.json delete mode 100644 build/css/style.css delete mode 100644 build/css/style.css.map delete mode 100644 build/css/style.min.css delete mode 100644 build/js/built.js delete mode 100644 js/modules/Camera.js delete mode 100644 js/modules/Clipboard.js delete mode 100644 js/modules/Contextmenu.js delete mode 100644 js/modules/Draw.js delete mode 100644 js/modules/File.js delete mode 100644 js/modules/Layers.js delete mode 100644 js/modules/Resizer.js delete mode 100644 js/modules/Select.js delete mode 100644 js/modules/Shapes.js delete mode 100644 js/modules/Text.js delete mode 100644 js/modules/WebDraft.js delete mode 100644 js/modules/events.module.js delete mode 100644 js/modules/keys.module.js delete mode 100644 js/start.js delete mode 100644 less/camera.less delete mode 100644 less/contextmenu.less delete mode 100644 less/hints.less delete mode 100644 less/info.less delete mode 100644 less/inputs.less delete mode 100644 less/layers.less delete mode 100644 less/resizer.less delete mode 100644 less/shadow-changer.less delete mode 100644 less/sliders.less delete mode 100644 less/statusbar.less delete mode 100644 less/style.less delete mode 100644 less/text-options.less delete mode 100644 less/tools.less delete mode 100644 less/variables.less delete mode 100644 parts/buttons.part.html delete mode 100644 parts/camera.part.html delete mode 100644 parts/color-picker.part.html delete mode 100644 parts/info.part.html delete mode 100644 parts/layers.part.html delete mode 100644 parts/resizer.part.html delete mode 100644 parts/sliders.part.html delete mode 100644 parts/text-options.part.html delete mode 100644 pic/transparent.png rename {app-v3/src => src}/core/canvas-drawing.ts (100%) rename {app-v3/src => src}/core/editor-geometry.ts (100%) rename {app-v3/src => src}/core/flood-fill.ts (100%) rename {app-v3/src => src}/core/history-manager.ts (100%) rename {app-v3/src => src}/core/i18n.ts (100%) rename {app-v3/src => src}/core/layer-manager.ts (100%) rename {app-v3/src => src}/core/layer-transforms.ts (100%) rename {app-v3/src => src}/core/project-file.ts (100%) rename {app-v3/src => src}/core/types.ts (100%) rename {app-v3/src => src}/core/webdraft-editor.ts (100%) rename {app-v3/src => src}/i18n/en/camera.ts (100%) rename {app-v3/src => src}/i18n/en/common.ts (100%) rename {app-v3/src => src}/i18n/en/edit.ts (100%) rename {app-v3/src => src}/i18n/en/file.ts (100%) rename {app-v3/src => src}/i18n/en/image.ts (100%) rename {app-v3/src => src}/i18n/en/index.ts (100%) rename {app-v3/src => src}/i18n/en/layer.ts (100%) rename {app-v3/src => src}/i18n/en/layers.ts (100%) rename {app-v3/src => src}/i18n/en/menu.ts (100%) rename {app-v3/src => src}/i18n/en/picker.ts (100%) rename {app-v3/src => src}/i18n/en/saveDialog.ts (100%) rename {app-v3/src => src}/i18n/en/toolbar.ts (100%) rename {app-v3/src => src}/i18n/pl/camera.ts (100%) rename {app-v3/src => src}/i18n/pl/common.ts (100%) rename {app-v3/src => src}/i18n/pl/edit.ts (100%) rename {app-v3/src => src}/i18n/pl/file.ts (100%) rename {app-v3/src => src}/i18n/pl/image.ts (100%) rename {app-v3/src => src}/i18n/pl/index.ts (100%) rename {app-v3/src => src}/i18n/pl/layer.ts (100%) rename {app-v3/src => src}/i18n/pl/layers.ts (100%) rename {app-v3/src => src}/i18n/pl/menu.ts (100%) rename {app-v3/src => src}/i18n/pl/picker.ts (100%) rename {app-v3/src => src}/i18n/pl/saveDialog.ts (100%) rename {app-v3/src => src}/i18n/pl/toolbar.ts (100%) rename {app-v3/src => src}/i18n/types.ts (100%) rename {app-v3/src => src}/main.ts (100%) rename {app-v3/src => src}/styles.css (100%) rename {app-v3/src => src}/ui/camera-panel.ts (100%) rename {app-v3/src => src}/ui/color-picker.ts (100%) rename {app-v3/src => src}/ui/keyboard-shortcuts.ts (100%) rename {app-v3/src => src}/ui/lang-state.ts (100%) rename {app-v3/src => src}/ui/layers-panel.ts (100%) rename {app-v3/src => src}/ui/menu-bar.ts (100%) rename {app-v3/src => src}/ui/resize-dialog.ts (100%) rename {app-v3/src => src}/ui/right-panel.ts (100%) rename {app-v3/src => src}/ui/save-dialog.ts (100%) rename {app-v3/src => src}/ui/status-toasts.ts (100%) rename {app-v3/src => src}/ui/text-toolbar.ts (100%) rename {app-v3/src => src}/ui/toolbar-controls.ts (100%) rename {app-v3/src => src}/ui/toolbar-sections.ts (100%) rename {app-v3/src => src}/ui/toolbar.ts (100%) rename {app-v3/tests => tests}/editor-geometry.test.mjs (100%) rename {app-v3/tests => tests}/flood-fill.test.mjs (100%) rename {app-v3/tests => tests}/history-manager.test.mjs (100%) rename {app-v3/tests => tests}/layer-transforms.test.mjs (100%) rename {app-v3/tests => tests}/test-support.mjs (100%) rename app-v3/tsconfig.json => tsconfig.json (100%) delete mode 100644 vendor/canvas-to-blob/canvas-toBlob.js delete mode 100644 vendor/file-saver/FileSaver.min.js delete mode 100644 vendor/fontawesome/css/fa-brands.css delete mode 100644 vendor/fontawesome/css/fa-brands.min.css delete mode 100644 vendor/fontawesome/css/fa-regular.css delete mode 100644 vendor/fontawesome/css/fa-regular.min.css delete mode 100644 vendor/fontawesome/css/fa-solid.css delete mode 100644 vendor/fontawesome/css/fa-solid.min.css delete mode 100644 vendor/fontawesome/css/fontawesome-all.css delete mode 100644 vendor/fontawesome/css/fontawesome-all.min.css delete mode 100644 vendor/fontawesome/css/fontawesome.css delete mode 100644 vendor/fontawesome/css/fontawesome.min.css delete mode 100644 vendor/fontawesome/webfonts/fa-brands-400.eot delete mode 100644 vendor/fontawesome/webfonts/fa-brands-400.svg delete mode 100644 vendor/fontawesome/webfonts/fa-brands-400.ttf delete mode 100644 vendor/fontawesome/webfonts/fa-brands-400.woff delete mode 100644 vendor/fontawesome/webfonts/fa-brands-400.woff2 delete mode 100644 vendor/fontawesome/webfonts/fa-regular-400.eot delete mode 100644 vendor/fontawesome/webfonts/fa-regular-400.svg delete mode 100644 vendor/fontawesome/webfonts/fa-regular-400.ttf delete mode 100644 vendor/fontawesome/webfonts/fa-regular-400.woff delete mode 100644 vendor/fontawesome/webfonts/fa-regular-400.woff2 delete mode 100644 vendor/fontawesome/webfonts/fa-solid-900.eot delete mode 100644 vendor/fontawesome/webfonts/fa-solid-900.svg delete mode 100644 vendor/fontawesome/webfonts/fa-solid-900.ttf delete mode 100644 vendor/fontawesome/webfonts/fa-solid-900.woff delete mode 100644 vendor/fontawesome/webfonts/fa-solid-900.woff2 delete mode 100644 vendor/html2canvas/html2canvas.js delete mode 100644 vendor/jquery-ui/css/jquery-ui-1.12.1.min.css delete mode 100644 vendor/jquery-ui/js/jquery-ui-1.12.1.min.js delete mode 100644 vendor/jquery/jquery-3.2.1.min.js delete mode 160000 vendor/rtcoder/colorpicker diff --git a/.gitignore b/.gitignore index 789e86d..1eae0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -/nbproject/private/ -nbproject/ -node_modules/* \ No newline at end of file +dist/ +node_modules/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 9517a29..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "vendor/rtcoder/colorpicker"] - path = vendor/rtcoder/colorpicker - url = https://github.com/rtcoder/Color-Picker.git diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 8cdaa6e..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function (grunt) { - grunt.initConfig({ - less: { - development: { - options: { - paths: ["./less"], - yuicompress: true, - sourceMap: true - }, - files: { - "./build/css/style.css": "./less/style.less" - } - } - }, - cssmin: { - options: { - mergeIntoShorthands: false, - roundingPrecision: -1 - }, - target: { - files: { - './build/css/style.min.css': './build/css/style.css' - } - } - }, - concat: { - options: { - separator: "\n", - }, - dist: { - src: ["./js/modules/*", "./js/*"], - dest: './build/js/built.js', - }, - }, - watch: { - lessCompile: { - files: "./less/*", - tasks: ["less"] - }, - minifyCss: { - files: "./build/css/style.css", - tasks: ["cssmin"] - }, - concatJs: { - files: ["./js/*", "./js/modules/*"], - tasks: ["concat"] - }, - } - }); - grunt.loadNpmTasks('grunt-contrib-less'); - grunt.loadNpmTasks('grunt-contrib-cssmin'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-concat'); -}; \ No newline at end of file diff --git a/app-v3/.gitignore b/app-v3/.gitignore deleted file mode 100644 index 1eae0cf..0000000 --- a/app-v3/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -node_modules/ diff --git a/app-v3/index.html b/app-v3/index.html deleted file mode 100644 index 3321ef7..0000000 --- a/app-v3/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - WebDraft v3 - - -
- - - diff --git a/app-v3/package-lock.json b/app-v3/package-lock.json deleted file mode 100644 index 0d31313..0000000 --- a/app-v3/package-lock.json +++ /dev/null @@ -1,1002 +0,0 @@ -{ - "name": "webdraft-v3", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "webdraft-v3", - "version": "0.1.0", - "devDependencies": { - "typescript": "^5.5.4", - "vite": "^5.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", - "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", - "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", - "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", - "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", - "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", - "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", - "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", - "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", - "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", - "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", - "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", - "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", - "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", - "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", - "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", - "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", - "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", - "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", - "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", - "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", - "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", - "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", - "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", - "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", - "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", - "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.61.1", - "@rollup/rollup-android-arm64": "4.61.1", - "@rollup/rollup-darwin-arm64": "4.61.1", - "@rollup/rollup-darwin-x64": "4.61.1", - "@rollup/rollup-freebsd-arm64": "4.61.1", - "@rollup/rollup-freebsd-x64": "4.61.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", - "@rollup/rollup-linux-arm-musleabihf": "4.61.1", - "@rollup/rollup-linux-arm64-gnu": "4.61.1", - "@rollup/rollup-linux-arm64-musl": "4.61.1", - "@rollup/rollup-linux-loong64-gnu": "4.61.1", - "@rollup/rollup-linux-loong64-musl": "4.61.1", - "@rollup/rollup-linux-ppc64-gnu": "4.61.1", - "@rollup/rollup-linux-ppc64-musl": "4.61.1", - "@rollup/rollup-linux-riscv64-gnu": "4.61.1", - "@rollup/rollup-linux-riscv64-musl": "4.61.1", - "@rollup/rollup-linux-s390x-gnu": "4.61.1", - "@rollup/rollup-linux-x64-gnu": "4.61.1", - "@rollup/rollup-linux-x64-musl": "4.61.1", - "@rollup/rollup-openbsd-x64": "4.61.1", - "@rollup/rollup-openharmony-arm64": "4.61.1", - "@rollup/rollup-win32-arm64-msvc": "4.61.1", - "@rollup/rollup-win32-ia32-msvc": "4.61.1", - "@rollup/rollup-win32-x64-gnu": "4.61.1", - "@rollup/rollup-win32-x64-msvc": "4.61.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - } - } -} diff --git a/app-v3/package.json b/app-v3/package.json deleted file mode 100644 index e5381fb..0000000 --- a/app-v3/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "webdraft-v3", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 127.0.0.1", - "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs && node tests/flood-fill.test.mjs", - "check": "npm test && npm run build", - "build": "tsc && vite build", - "preview": "vite preview --host 127.0.0.1" - }, - "devDependencies": { - "typescript": "^5.5.4", - "vite": "^5.4.0" - } -} diff --git a/build/css/style.css b/build/css/style.css deleted file mode 100644 index d70deb6..0000000 --- a/build/css/style.css +++ /dev/null @@ -1,1025 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Open+Sans); -input[type=checkbox] + label { - cursor: pointer; - color: #A9A9A9; - width: 100%; - background: transparent; - text-align: left; - border: none; - border-radius: 0px; -} -input[type=checkbox] + label .fa-circle, -input[type=checkbox] + label .fa-check-circle { - float: left; - text-align: center; - width: 30px; - vertical-align: middle; - margin: 0px 0px 0px 10px; - font-size: 18px; - height: 20px; - line-height: 20px; -} -input[type=checkbox] + label .fa-check-circle { - display: none; -} -input[type=checkbox] + label #l { - margin-left: 10px; - height: 20px; - line-height: 20px; -} -input[type=checkbox] + label span { - width: 100%; - height: 20px; - text-align: left; -} -input[type=checkbox] + label span.label { - width: auto; -} -input[type=checkbox]:checked + label { - font-weight: bold; - color: #0052CC; -} -input[type=checkbox]:checked + label .fa-circle { - display: none; -} -input[type=checkbox]:checked + label .fa-check-circle { - display: block; -} -input[type=checkbox]:hover + label { - color: #FFFFFF; -} -input[type=checkbox]:hover + label .fa-circle { - display: none; -} -input[type=checkbox]:hover + label .fa-check-circle { - display: block; -} -input[type=range] { - -webkit-appearance: none; - width: calc(100% - 20px); - margin: 0 10px; -} -input[type=range]::-webkit-slider-runnable-track { - width: 230px; - height: 10px; - background: #DDDDDD; - border: none; - border-radius: 0px; -} -input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: #0052CC; -} -input[type=range]::-moz-range-track { - width: 230px; - height: 10px; - background: #DDDDDD; - border: none; -} -input[type=range]::-moz-range-thumb { - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: #0052CC; -} -input[type=range]::-ms-track { - width: 230px; - height: 10px; - background: transparent; - border-color: transparent; - border-width: 0; - color: transparent; -} -input[type=range]::-ms-thumb { - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: #0052CC; -} -input[type=range]:-moz-focusring { - outline: none; -} -input[type=range]::-ms-fill-lower { - background: #DDDDDD; -} -input[type=range]::-ms-fill-upper { - background: #DDDDDD; -} -input[type=range]:focus { - outline: none; -} -input[type=range]:focus::-ms-fill-lower { - background: #888888; -} -input[type=range]:focus::-webkit-slider-runnable-track { - background: #CCCCCC; -} -input[type=range]:focus::-ms-fill-upper { - background: #CCCCCC; -} -input[type=color] { - visibility: hidden; - width: 0px; - height: 0px; - position: absolute; -} -input#fileUploaderInput { - display: none; -} -input#isFillSet { - display: none; -} -input#isShadow { - display: none; -} -#toolsGroup { - width: 200px; - height: calc(100vh - 20px); - background: #2C2B2B; - float: left; -} -#toolsGroup #previewColorSampler { - width: 100%; - height: 60px; - background: 0 30px url('/pic/transparent.png'); - z-index: 1; - display: none; -} -#toolsGroup #previewColorSampler #textColorSampler { - width: 100%; - float: left; - height: 30px; - background: #2C2B2B; - line-height: 30px; - text-align: center; - font-size: 13px; - color: #EEEEEE; -} -#toolsGroup #previewColorSampler #colorBoxSampler { - width: 100%; - height: 30px; - float: left; -} -#toolsGroup .tools { - width: 100%; - background: #2C2B2B; - float: left; - z-index: 4; - color: #A9A9A9; -} -#toolsGroup .tools .title { - width: 100%; - height: 20px; - line-height: 20px; - float: left; - font-size: 15px; - text-align: center; - background: #EEEEEE; - color: #2E3436; -} -#toolsGroup .tools .title .fas, -#toolsGroup .tools .title .fab, -#toolsGroup .tools .title .far { - position: relative; - height: inherit; - line-height: inherit; - left: 0; - top: 0; - text-align: center; -} -#toolsGroup .tools #toolsBtns { - width: 100%; - float: left; -} -#toolsGroup .tools #toolsBtns .primaryTool, -#toolsGroup .tools #toolsBtns .paintTools { - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: #A9A9A9; - cursor: pointer; -} -#toolsGroup .tools #toolsBtns .primaryTool .fas, -#toolsGroup .tools #toolsBtns .paintTools .fas, -#toolsGroup .tools #toolsBtns .primaryTool .fab, -#toolsGroup .tools #toolsBtns .paintTools .fab, -#toolsGroup .tools #toolsBtns .primaryTool .far, -#toolsGroup .tools #toolsBtns .paintTools .far { - font-size: 15px; -} -#toolsGroup .tools #toolsBtns .primaryTool:hover, -#toolsGroup .tools #toolsBtns .paintTools:hover { - color: #FFFFFF; -} -#toolsGroup .tools #toolsBtns .paintTools.active { - cursor: default; - color: #0052CC; - background: #1C1B1B; -} -#toolsGroup .tools #toolsBtns .primaryTool:active { - cursor: default; - color: #0052CC; - background: #1C1B1B; -} -#toolsGroup .tools #radioBtns { - width: 100%; - float: left; -} -#toolsGroup .tools #radioBtns #shadowInput, -#toolsGroup .tools #radioBtns #fillShapeInput { - width: 100%; - line-height: 20px; - float: left; - padding: 5px 0px; - font-size: 12px; -} -#toolsGroup .tools #radioBtns #shadowInput *, -#toolsGroup .tools #radioBtns #fillShapeInput * { - cursor: pointer; - float: left; -} -#toolsGroup .tools #radioBtns #fillShapeInput { - display: none; -} -#toolsGroup .tools #colorPicker { - width: 100%; - float: left; -} -#toolsGroup .tools #colorPicker .colorTab { - width: 100%; - height: 25px; - line-height: 25px; - font-size: 14px; - cursor: pointer; -} -#toolsGroup .tools #colorPicker .colorTab .color { - border-radius: 100%; - width: 15px; - height: 15px; - margin: 5px 10px; - float: left; - border: 1px solid #CCCCCC; -} -#toolsGroup .tools #colorPicker .colorTab#generalColor .color { - background: #000000; -} -#toolsGroup .tools #colorPicker .colorTab#shadowColor { - display: none; -} -#toolsGroup .tools #colorPicker .colorTab#shadowColor .color { - background: #232324; -} -#toolsGroup .tools #colorPicker .colorTab#fillColor { - display: none; -} -#toolsGroup .tools #colorPicker .colorTab#fillColor .color { - background: #FFFFFF; -} -#toolsGroup .tools #colorPicker .colorTab:hover { - color: #0052CC; - background: #1C1B1B; -} -#textOptions { - display: none; -} -#textOptions #alignBtns { - width: 100%; - float: left; -} -#textOptions #alignBtns .textPostionTool { - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: #A9A9A9; - cursor: pointer; -} -#textOptions #alignBtns .textPostionTool .fas, -#textOptions #alignBtns .textPostionTool .fab, -#textOptions #alignBtns .textPostionTool .far { - font-size: 15px; -} -#textOptions #alignBtns .textPostionTool:hover, -#textOptions #alignBtns .textPostionTool.active { - color: #FFFFFF; -} -#textOptions #fontStyleBtns { - width: 100%; - float: left; -} -#textOptions #fontStyleBtns .styleTool { - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: #A9A9A9; - cursor: pointer; -} -#textOptions #fontStyleBtns .styleTool .fas, -#textOptions #fontStyleBtns .styleTool .fab, -#textOptions #fontStyleBtns .styleTool .far { - font-size: 15px; -} -#textOptions #fontStyleBtns .styleTool:hover, -#textOptions #fontStyleBtns .styleTool.active { - color: #FFFFFF; -} -#textRectangle { - display: none; - position: absolute; - z-index: 2; - border: 1px solid #000000; - font-size: 8mm; - cursor: move; -} -#layers .title { - width: 100%; - height: 20px; - line-height: 20px; - float: left; - font-size: 15px; - text-align: center; - background: #EEEEEE; - color: #2E3436; -} -#layers #layersBtns { - width: 100%; - background: #2C2B2B; - float: left; - z-index: 4; - color: #A9A9A9; -} -#layers #layersBtns .layerTool { - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: #A9A9A9; - cursor: pointer; -} -#layers #layersBtns .layerTool#addLayer { - color: #16c600; -} -#layers #layersBtns .layerTool#delLayer { - color: #C50000; -} -#layers #layersBtns .layerTool:hover { - color: #FFFFFF; -} -#layers #layersBtns .layerTool .fas, -#layers #layersBtns .layerTool .fab, -#layers #layersBtns .layerTool .far { - font-size: 15px; -} -#layers #listLayers { - width: 100%; - background: #2C2B2B; - float: left; - z-index: 4; - color: #A9A9A9; - max-height: calc(50vh - 170px); - overflow: auto; - top: 50px; -} -#layers #listLayers .layerView { - position: relative; - width: 100%; - height: 60px; - margin: 10px auto; - background: transparent; - text-align: center; - cursor: pointer; -} -#layers #listLayers .layerView.active { - cursor: default; - background-color: rgba(0, 0, 0, 0.1); -} -#layers #listLayers .layerView.active:hover { - cursor: default; - background-color: rgba(0, 0, 0, 0.1); -} -#layers #listLayers .layerView.active .imgLayerContainer { - border-color: #0021CC; -} -#layers #listLayers .layerView:hover { - background-color: rgba(0, 0, 0, 0.3); -} -#layers #listLayers .layerView .imgLayerContainer { - float: left; - border: 1px solid #CCCCCC; - width: 60px; - height: 60px; -} -#layers #listLayers .layerView .imgLayerContainer img.imgLayer { - float: left; - max-width: 58px; - max-height: 58px; -} -#layers #listLayers .layerView .hideLayer, -#layers #listLayers .layerView .showLayer { - float: left; - margin: 21px 5px; - width: 18px; - height: 18px; - line-height: 18px !important; - text-align: center; - cursor: pointer; -} -#layers #listLayers .layerView .showLayer { - color: #C50000; -} -#layers #listLayers .layerView .showLayer:hover { - color: #16c600; -} -#layers #listLayers .layerView .hideLayer { - color: #16c600; -} -#layers #listLayers .layerView .hideLayer:hover { - color: #C50000; -} -#sliders { - width: 100%; - float: left; -} -#sliders .sliderContainer { - width: 100%; - height: 45px; - float: left; -} -#sliders .sliderContainer#shadow_slider { - display: none; -} -#sliders .sliderContainer#sensitivityPoints_slider { - display: none; -} -#sliders .sliderContainer#fillOpacity_slider { - display: none; -} -#sliders .sliderContainer .sliderVal { - width: 100%; - height: 20px; - margin-top: 5px; - line-height: 20px; - word-wrap: break-word; - font-size: 13px; - float: left; - text-align: center; -} -#info { - width: 350px; - height: 100vh; - z-index: 5; - position: absolute; - display: none; - overflow: auto; - background: #2C2B2B; - border-left: 1px solid #A9A9A9; - color: #FFFFFF; - top: 0px; - right: 0px; -} -@media all and (max-width: 400px) { - #info { - width: 100vw; - } -} -#info .close-info { - position: absolute; - right: 0; - cursor: pointer; - margin: 5px; -} -#info .info-section { - width: 100%; - float: left; - margin: 5px 0px; -} -#info .info-section .info-section-title { - width: 100%; - float: left; - text-align: center; -} -#info .info-section .info-section-line { - width: 100%; - float: left; -} -#info .info-section .info-section-line .info-section-shortcut { - width: 30%; - float: left; - padding: 2px 5px; - text-align: right; -} -#info .info-section .info-section-line .info-section-shortcut span { - padding: 0 5px; - min-width: 30px; - height: 20px; - border-radius: 2px; - margin: 15px 5px; - text-align: center; - line-height: 20px; - font-size: 12px; - background: #F1F1F1; - border: 1px solid #CCCCCC; - color: #2C2B2B; -} -#info .info-section .info-section-line .info-section-shortcut-describe { - width: 70%; - float: left; - padding: 2px 5px; -} -#resizer { - width: 300px; - height: 210px; - position: absolute; - top: 0; - left: 0; - background: #2C2B2B; - display: none; - padding: 10px; - z-index: 4; - margin-left: calc((100vw - 300px) / 2); - margin-top: calc((100vh - 210px) / 2); -} -#resizer input[type=number] { - width: 250px; - height: 30px; - margin: 10px 15px; - border: none; - cursor: text; - text-align: center; - float: left; -} -#resizer #resizeinfo { - width: 250px; - height: 30px; - line-height: 30px; - text-align: center; - color: #CCCCCC; - float: left; - margin: 0px 20px; -} -#resizer .buttonAction { - float: left; - width: 120px; - height: 30px; - line-height: 30px; - font-size: 15px; - margin: 10px 10px; - text-align: center; - background: #CCCCCC; - cursor: pointer; -} -#resizer .buttonAction:hover { - background: #0052CC; - color: #CCCCCC; -} -#hint { - width: 100vw; - height: 50px; - position: absolute; - bottom: 20px; - left: 0; - background: #2C2B2B; - display: none; -} -#hint .hintGroup { - width: 100%; - height: 100%; - float: left; - display: none; -} -#hint .hintGroup .keyboardButton { - float: left; - padding: 0 5px; - min-width: 30px; - height: 20px; - background: #F1F1F1; - box-sizing: border-box; - border: 1px solid #CCCCCC; - border-radius: 2px; - margin: 15px 5px; - color: #2C2B2B; - text-align: center; - line-height: 20px; - font-size: 12px; -} -#hint .hintGroup .describeHint { - width: auto; - float: left; - height: 50px; - line-height: 50px; - margin: 0 5px 0 0; - color: #F1F1E4; - font-size: 12px; -} -#shadowSquare { - width: 200px; - height: 200px; - position: relative; - background: rgba(0, 0, 0, 0.4); - float: left; - display: none; -} -#shadowSquare #shadowDot { - cursor: move; - background: #FFFFFF; - border-radius: 100%; - width: 10px; - height: 10px; - position: absolute; - top: 95px; - left: 95px; - z-index: 2; -} -#shadowSquare::after, -#shadowSquare::before { - position: absolute; - content: ' '; - background: #A9A9A9; - z-index: 1; -} -#shadowSquare::after { - width: 200px; - height: 1px; - top: 50%; - left: 0px; -} -#shadowSquare::before { - width: 1px; - height: 200px; - top: 0%; - left: 50%; -} -#statusbar { - width: 100%; - background: #2C2B2B; - height: 20px; - line-height: 20px; - float: left; - position: absolute; - bottom: 0; - font-size: 13px; - color: #CCCCCC; -} -#statusbar #mousePosition { - float: left; - width: 100px; - height: inherit; - line-height: inherit; - text-align: center; -} -#statusbar #author { - height: inherit; - line-height: inherit; - float: right; - right: 0; - margin-right: 20px; -} -#statusbar #author a { - color: #EEEEEE; - cursor: pointer; -} -#camera { - box-shadow: 0px 0px 5px 2px #6f6f6f; - width: 740px; - max-width: 100vw; - height: 560px; - max-height: 100vh; - background: #2C2B2B; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - position: absolute; - z-index: 99; - display: none; -} -#camera.opened { - display: block; -} -#camera.fullscreen { - width: 100vw; - height: 100vh; - top: 0; - left: 0; - transform: translate(0, 0); -} -#camera.fullscreen .cameraContent .videoContainer { - width: 100vw; - height: calc(100vh - 130px); -} -#camera.fullscreen .cameraContent .filters { - width: 100vw; - height: 50px; - line-height: 50px; -} -#camera.fullscreen .cameraContent .filters label span.filter { - margin: 15px 5px; -} -#camera #cameraHeader { - width: 100%; - height: 30px; -} -#camera #cameraHeader #cameraTitle { - width: calc(100% - 70px); - float: left; - height: 30px; - line-height: 30px; - padding: 0px 5px; - color: #FFFFFF; -} -#camera #cameraHeader button { - width: 30px; - height: 30px; - border: none; - display: inline-block; - cursor: pointer; - float: right; -} -#camera #cameraHeader button i { - cursor: pointer; -} -#camera #cameraHeader button#closeCamera { - background: #C50000; - color: #FFFFFF; -} -#camera #cameraHeader button#switchFullscreen { - background: #0021CC; - color: #FFFFFF; - margin-right: 10px; -} -#camera .cameraContent { - width: 100%; - float: left; - margin-top: 10px; -} -#camera .cameraContent .videoContainer { - width: calc(100% - 100px); - float: left; - position: relative; -} -#camera .cameraContent .videoContainer canvas#snapImage, -#camera .cameraContent .videoContainer canvas#frame, -#camera .cameraContent .videoContainer canvas#additional, -#camera .cameraContent .videoContainer video { - width: 100%; - max-width: 100vw; - max-height: calc(100vh - 130px); - float: left; -} -#camera .cameraContent .videoContainer canvas#snapImage, -#camera .cameraContent .videoContainer canvas#frame { - display: none; -} -#camera .cameraContent .videoContainer canvas#additional { - display: block; - position: absolute; - top: 0; - left: 0; -} -#camera .cameraContent .filters { - width: 100px; - float: left; -} -#camera .cameraContent .filters label input { - display: none; -} -#camera .cameraContent .filters label input:checked + span { - background: #0052CC; - color: #FFFFFF; -} -#camera .cameraContent .filters label span.filter { - width: 70px; - height: 20px; - margin: 0px 15px 5px; - cursor: pointer; - background: #EEEEEE; - line-height: 20px; - text-align: center; - font-size: 13px; - float: left; -} -#camera .cameraContent #cameraButtons { - float: left; - width: 100%; - height: 50px; - line-height: 50px; -} -#camera .cameraContent #cameraButtons button { - min-width: 50px; - border: none; - display: none; - cursor: pointer; -} -#camera .cameraContent #cameraButtons button * { - cursor: pointer; -} -#camera .cameraContent #cameraButtons button#snap { - display: inline-block; -} -#camera .cameraContent #cameraButtons button#applySnap { - background: #129a01; - color: #FFFFFF; -} -#camera .cameraContent #cameraButtons button#cancelSnap { - background: #C50000; - color: #FFFFFF; -} -#camera .cameraContent #cameraButtons button#saveSnapOnComputer { - background: #0052CC; - color: #FFFFFF; -} -#camera.snapped #cameraButtons button { - display: inline-block; -} -#camera.snapped #cameraButtons button#snap { - display: none; -} -#camera.snapped .cameraContent .videoContainer canvas#additional, -#camera.snapped .cameraContent .videoContainer video#video { - display: none; -} -#camera.snapped .cameraContent .videoContainer canvas#snapImage { - display: block; -} -menu#contextmenu { - display: none; - width: 230px; - height: auto; - position: fixed; - top: 50%; - left: 50%; - background: #fff; - z-index: 10; -} -menu#contextmenu ul li { - cursor: pointer; - float: left; - height: 30px; - line-height: 30px; - position: relative; - width: 100%; - background: #fff; - color: #000000; -} -menu#contextmenu ul li * { - cursor: pointer; -} -menu#contextmenu ul li .content { - width: 100%; - float: left; -} -menu#contextmenu ul li .content span { - height: inherit; - float: left; -} -menu#contextmenu ul li .content span.icon { - width: 20px; -} -menu#contextmenu ul li .content span.icon i { - float: left; - width: 20px; - height: 30px; - line-height: 30px; -} -menu#contextmenu ul li .content span.text { - width: calc(100% - 130px); -} -menu#contextmenu ul li .content span.shortcut { - text-align: right; - text-transform: uppercase; - width: 70px; - font-size: 11px; - color: #2E3436; -} -menu#contextmenu ul li .content span.arrow { - color: #2E3436; - width: 10px; - margin: 0px 5px; - font-size: 11px; - visibility: hidden; - opacity: 0; -} -menu#contextmenu ul li .submenu { - width: 230px; - position: absolute; - display: none; - top: 0; - left: 230px; -} -menu#contextmenu ul li .submenu + .content span.arrow.left-arrow { - visibility: hidden; - opacity: 0; -} -menu#contextmenu ul li .submenu + .content span.arrow.right-arrow { - visibility: visible; - opacity: 1; - float: right; -} -menu#contextmenu ul li.disabled { - color: #888888; -} -menu#contextmenu ul li:not(.disabled):hover { - color: #0052CC; - background-color: #dedede; -} -menu#contextmenu ul li:not(.disabled):hover .submenu { - display: block; - color: #000000; -} -menu#contextmenu.left-submenu ul li .submenu { - left: -230px; -} -menu#contextmenu.left-submenu ul li .submenu + .content span.arrow.left-arrow { - visibility: visible; - opacity: 1; -} -menu#contextmenu.left-submenu ul li .submenu + .content span.arrow.right-arrow { - visibility: hidden; - opacity: 0; -} -* { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: default; - font-family: 'Open Sans', sans-serif; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - margin: 0; - padding: 0; -} -ul { - list-style: none; -} -html body { - top: 0; - left: 0; - width: 100%; - height: 100%; - float: left; - position: fixed; - overflow: hidden; -} -html body #content { - float: left; - background: #373636; - overflow: auto; - width: calc(100vw - 200px); - height: calc(100vh - 20px); -} -html body #content #drawHandler { - border: 1px dotted #0052CC; - position: relative; - overflow: hidden; -} -html body #content #drawHandler #draw, -html body #content #drawHandler #drawHandler, -html body #content #drawHandler #eventHandler { - width: 600px; - height: 400px; - overflow: hidden; - position: relative; - /*cursor: crosshair;*/ -} -html body #content #drawHandler #eventHandler { - z-index: 3; - position: absolute; -} -html body #content #drawHandler #draw canvas { - position: absolute; - cursor: crosshair; - border: 1px dotted #0052CC; -} -html body #content #drawHandler #draw canvas.invisible { - visibility: hidden; -} -html body #eraseRect, -html body #prepareRect, -html body #prepareCircle, -html body #selectRectangle { - display: none; - position: absolute; - z-index: 2; - border: 1px solid #000000; -} -html body #selectRectangle { - cursor: move; -} -html body #eraseRect { - border-radius: 100%; - border-color: #CCCCCC; -} -/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/build/css/style.css.map b/build/css/style.css.map deleted file mode 100644 index f9e240b..0000000 --- a/build/css/style.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["less/inputs.less","less/tools.less","less/text-options.less","less/layers.less","less/sliders.less","less/info.less","less/resizer.less","less/hints.less","less/shadow-changer.less","less/statusbar.less","less/camera.less","less/contextmenu.less","./less/style.less"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/build/css/style.min.css b/build/css/style.min.css deleted file mode 100644 index e6ff274..0000000 --- a/build/css/style.min.css +++ /dev/null @@ -1 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Open+Sans);input[type=checkbox]+label{cursor:pointer;color:#a9a9a9;width:100%;background:0 0;text-align:left;border:none;border-radius:0}input[type=checkbox]+label .fa-check-circle,input[type=checkbox]+label .fa-circle{float:left;text-align:center;width:30px;vertical-align:middle;margin:0 0 0 10px;font-size:18px;height:20px;line-height:20px}input[type=checkbox]+label .fa-check-circle{display:none}input[type=checkbox]+label #l{margin-left:10px;height:20px;line-height:20px}input[type=checkbox]+label span{width:100%;height:20px;text-align:left}input[type=checkbox]+label span.label{width:auto}input[type=checkbox]:checked+label{font-weight:700;color:#0052cc}input[type=checkbox]:checked+label .fa-circle{display:none}input[type=checkbox]:checked+label .fa-check-circle{display:block}input[type=checkbox]:hover+label{color:#fff}input[type=checkbox]:hover+label .fa-circle{display:none}input[type=checkbox]:hover+label .fa-check-circle{display:block}input[type=range]{-webkit-appearance:none;width:calc(100% - 20px);margin:0 10px}input[type=range]::-webkit-slider-runnable-track{width:230px;height:10px;background:#ddd;border:none;border-radius:0}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:10px;width:10px;border-radius:0;background:#0052cc}input[type=range]::-moz-range-track{width:230px;height:10px;background:#ddd;border:none}input[type=range]::-moz-range-thumb{border:none;height:10px;width:10px;border-radius:0;background:#0052cc}input[type=range]::-ms-track{width:230px;height:10px;background:0 0;border-color:transparent;border-width:0;color:transparent}input[type=range]::-ms-thumb{border:none;height:10px;width:10px;border-radius:0;background:#0052cc}input[type=range]:-moz-focusring{outline:0}input[type=range]::-ms-fill-lower{background:#ddd}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]:focus{outline:0}input[type=range]:focus::-ms-fill-lower{background:#888}input[type=range]:focus::-webkit-slider-runnable-track{background:#ccc}input[type=range]:focus::-ms-fill-upper{background:#ccc}input[type=color]{visibility:hidden;width:0;height:0;position:absolute}input#fileUploaderInput{display:none}input#isFillSet{display:none}input#isShadow{display:none}#toolsGroup{width:200px;height:calc(100vh - 20px);background:#2c2b2b;float:left}#toolsGroup #previewColorSampler{width:100%;height:60px;background:0 30px url(/pic/transparent.png);z-index:1;display:none}#toolsGroup #previewColorSampler #textColorSampler{width:100%;float:left;height:30px;background:#2c2b2b;line-height:30px;text-align:center;font-size:13px;color:#eee}#toolsGroup #previewColorSampler #colorBoxSampler{width:100%;height:30px;float:left}#toolsGroup .tools{width:100%;background:#2c2b2b;float:left;z-index:4;color:#a9a9a9}#toolsGroup .tools .title{width:100%;height:20px;line-height:20px;float:left;font-size:15px;text-align:center;background:#eee;color:#2e3436}#toolsGroup .tools .title .fab,#toolsGroup .tools .title .far,#toolsGroup .tools .title .fas{position:relative;height:inherit;line-height:inherit;left:0;top:0;text-align:center}#toolsGroup .tools #toolsBtns{width:100%;float:left}#toolsGroup .tools #toolsBtns .paintTools,#toolsGroup .tools #toolsBtns .primaryTool{width:50px;height:30px;line-height:30px;text-align:center;float:left;color:#a9a9a9;cursor:pointer}#toolsGroup .tools #toolsBtns .paintTools .fab,#toolsGroup .tools #toolsBtns .paintTools .far,#toolsGroup .tools #toolsBtns .paintTools .fas,#toolsGroup .tools #toolsBtns .primaryTool .fab,#toolsGroup .tools #toolsBtns .primaryTool .far,#toolsGroup .tools #toolsBtns .primaryTool .fas{font-size:15px}#toolsGroup .tools #toolsBtns .paintTools:hover,#toolsGroup .tools #toolsBtns .primaryTool:hover{color:#fff}#toolsGroup .tools #toolsBtns .paintTools.active{cursor:default;color:#0052cc;background:#1c1b1b}#toolsGroup .tools #toolsBtns .primaryTool:active{cursor:default;color:#0052cc;background:#1c1b1b}#toolsGroup .tools #radioBtns{width:100%;float:left}#toolsGroup .tools #radioBtns #fillShapeInput,#toolsGroup .tools #radioBtns #shadowInput{width:100%;line-height:20px;float:left;padding:5px 0;font-size:12px}#toolsGroup .tools #radioBtns #fillShapeInput *,#toolsGroup .tools #radioBtns #shadowInput *{cursor:pointer;float:left}#toolsGroup .tools #radioBtns #fillShapeInput{display:none}#toolsGroup .tools #colorPicker{width:100%;float:left}#toolsGroup .tools #colorPicker .colorTab{width:100%;height:25px;line-height:25px;font-size:14px;cursor:pointer}#toolsGroup .tools #colorPicker .colorTab .color{border-radius:100%;width:15px;height:15px;margin:5px 10px;float:left;border:1px solid #ccc}#toolsGroup .tools #colorPicker .colorTab#generalColor .color{background:#000}#toolsGroup .tools #colorPicker .colorTab#shadowColor{display:none}#toolsGroup .tools #colorPicker .colorTab#shadowColor .color{background:#232324}#toolsGroup .tools #colorPicker .colorTab#fillColor{display:none}#toolsGroup .tools #colorPicker .colorTab#fillColor .color{background:#fff}#toolsGroup .tools #colorPicker .colorTab:hover{color:#0052cc;background:#1c1b1b}#textOptions{display:none}#textOptions #alignBtns{width:100%;float:left}#textOptions #alignBtns .textPostionTool{width:50px;height:30px;line-height:30px;text-align:center;float:left;color:#a9a9a9;cursor:pointer}#textOptions #alignBtns .textPostionTool .fab,#textOptions #alignBtns .textPostionTool .far,#textOptions #alignBtns .textPostionTool .fas{font-size:15px}#textOptions #alignBtns .textPostionTool.active,#textOptions #alignBtns .textPostionTool:hover{color:#fff}#textOptions #fontStyleBtns{width:100%;float:left}#textOptions #fontStyleBtns .styleTool{width:50px;height:30px;line-height:30px;text-align:center;float:left;color:#a9a9a9;cursor:pointer}#textOptions #fontStyleBtns .styleTool .fab,#textOptions #fontStyleBtns .styleTool .far,#textOptions #fontStyleBtns .styleTool .fas{font-size:15px}#textOptions #fontStyleBtns .styleTool.active,#textOptions #fontStyleBtns .styleTool:hover{color:#fff}#textRectangle{display:none;position:absolute;z-index:2;border:1px solid #000;font-size:8mm;cursor:move}#layers .title{width:100%;height:20px;line-height:20px;float:left;font-size:15px;text-align:center;background:#eee;color:#2e3436}#layers #layersBtns{width:100%;background:#2c2b2b;float:left;z-index:4;color:#a9a9a9}#layers #layersBtns .layerTool{width:50px;height:30px;line-height:30px;text-align:center;float:left;color:#a9a9a9;cursor:pointer}#layers #layersBtns .layerTool#addLayer{color:#16c600}#layers #layersBtns .layerTool#delLayer{color:#c50000}#layers #layersBtns .layerTool:hover{color:#fff}#layers #layersBtns .layerTool .fab,#layers #layersBtns .layerTool .far,#layers #layersBtns .layerTool .fas{font-size:15px}#layers #listLayers{width:100%;background:#2c2b2b;float:left;z-index:4;color:#a9a9a9;max-height:calc(50vh - 170px);overflow:auto;top:50px}#layers #listLayers .layerView{position:relative;width:100%;height:60px;margin:10px auto;background:0 0;text-align:center;cursor:pointer}#layers #listLayers .layerView.active{cursor:default;background-color:rgba(0,0,0,.1)}#layers #listLayers .layerView.active:hover{cursor:default;background-color:rgba(0,0,0,.1)}#layers #listLayers .layerView.active .imgLayerContainer{border-color:#0021cc}#layers #listLayers .layerView:hover{background-color:rgba(0,0,0,.3)}#layers #listLayers .layerView .imgLayerContainer{float:left;border:1px solid #ccc;width:60px;height:60px}#layers #listLayers .layerView .imgLayerContainer img.imgLayer{float:left;max-width:58px;max-height:58px}#layers #listLayers .layerView .hideLayer,#layers #listLayers .layerView .showLayer{float:left;margin:21px 5px;width:18px;height:18px;line-height:18px!important;text-align:center;cursor:pointer}#layers #listLayers .layerView .showLayer{color:#c50000}#layers #listLayers .layerView .showLayer:hover{color:#16c600}#layers #listLayers .layerView .hideLayer{color:#16c600}#layers #listLayers .layerView .hideLayer:hover{color:#c50000}#sliders{width:100%;float:left}#sliders .sliderContainer{width:100%;height:45px;float:left}#sliders .sliderContainer#shadow_slider{display:none}#sliders .sliderContainer#sensitivityPoints_slider{display:none}#sliders .sliderContainer#fillOpacity_slider{display:none}#sliders .sliderContainer .sliderVal{width:100%;height:20px;margin-top:5px;line-height:20px;word-wrap:break-word;font-size:13px;float:left;text-align:center}#info{width:350px;height:100vh;z-index:5;position:absolute;display:none;overflow:auto;background:#2c2b2b;border-left:1px solid #a9a9a9;color:#fff;top:0;right:0}@media all and (max-width:400px){#info{width:100vw}}#info .close-info{position:absolute;right:0;cursor:pointer;margin:5px}#info .info-section{width:100%;float:left;margin:5px 0}#info .info-section .info-section-title{width:100%;float:left;text-align:center}#info .info-section .info-section-line{width:100%;float:left}#info .info-section .info-section-line .info-section-shortcut{width:30%;float:left;padding:2px 5px;text-align:right}#info .info-section .info-section-line .info-section-shortcut span{padding:0 5px;min-width:30px;height:20px;border-radius:2px;margin:15px 5px;text-align:center;line-height:20px;font-size:12px;background:#f1f1f1;border:1px solid #ccc;color:#2c2b2b}#info .info-section .info-section-line .info-section-shortcut-describe{width:70%;float:left;padding:2px 5px}#resizer{width:300px;height:210px;position:absolute;top:0;left:0;background:#2c2b2b;display:none;padding:10px;z-index:4;margin-left:calc((100vw - 300px)/ 2);margin-top:calc((100vh - 210px)/ 2)}#resizer input[type=number]{width:250px;height:30px;margin:10px 15px;border:none;cursor:text;text-align:center;float:left}#resizer #resizeinfo{width:250px;height:30px;line-height:30px;text-align:center;color:#ccc;float:left;margin:0 20px}#resizer .buttonAction{float:left;width:120px;height:30px;line-height:30px;font-size:15px;margin:10px 10px;text-align:center;background:#ccc;cursor:pointer}#resizer .buttonAction:hover{background:#0052cc;color:#ccc}#hint{width:100vw;height:50px;position:absolute;bottom:20px;left:0;background:#2c2b2b;display:none}#hint .hintGroup{width:100%;height:100%;float:left;display:none}#hint .hintGroup .keyboardButton{float:left;padding:0 5px;min-width:30px;height:20px;background:#f1f1f1;box-sizing:border-box;border:1px solid #ccc;border-radius:2px;margin:15px 5px;color:#2c2b2b;text-align:center;line-height:20px;font-size:12px}#hint .hintGroup .describeHint{width:auto;float:left;height:50px;line-height:50px;margin:0 5px 0 0;color:#f1f1e4;font-size:12px}#shadowSquare{width:200px;height:200px;position:relative;background:rgba(0,0,0,.4);float:left;display:none}#shadowSquare #shadowDot{cursor:move;background:#fff;border-radius:100%;width:10px;height:10px;position:absolute;top:95px;left:95px;z-index:2}#shadowSquare::after,#shadowSquare::before{position:absolute;content:' ';background:#a9a9a9;z-index:1}#shadowSquare::after{width:200px;height:1px;top:50%;left:0}#shadowSquare::before{width:1px;height:200px;top:0;left:50%}#statusbar{width:100%;background:#2c2b2b;height:20px;line-height:20px;float:left;position:absolute;bottom:0;font-size:13px;color:#ccc}#statusbar #mousePosition{float:left;width:100px;height:inherit;line-height:inherit;text-align:center}#statusbar #author{height:inherit;line-height:inherit;float:right;right:0;margin-right:20px}#statusbar #author a{color:#eee;cursor:pointer}#camera{box-shadow:0 0 5px 2px #6f6f6f;width:740px;max-width:100vw;height:560px;max-height:100vh;background:#2c2b2b;top:50%;left:50%;transform:translate(-50%,-50%);position:absolute;z-index:99;display:none}#camera.opened{display:block}#camera.fullscreen{width:100vw;height:100vh;top:0;left:0;transform:translate(0,0)}#camera.fullscreen .cameraContent .videoContainer{width:100vw;height:calc(100vh - 130px)}#camera.fullscreen .cameraContent .filters{width:100vw;height:50px;line-height:50px}#camera.fullscreen .cameraContent .filters label span.filter{margin:15px 5px}#camera #cameraHeader{width:100%;height:30px}#camera #cameraHeader #cameraTitle{width:calc(100% - 70px);float:left;height:30px;line-height:30px;padding:0 5px;color:#fff}#camera #cameraHeader button{width:30px;height:30px;border:none;display:inline-block;cursor:pointer;float:right}#camera #cameraHeader button i{cursor:pointer}#camera #cameraHeader button#closeCamera{background:#c50000;color:#fff}#camera #cameraHeader button#switchFullscreen{background:#0021cc;color:#fff;margin-right:10px}#camera .cameraContent{width:100%;float:left;margin-top:10px}#camera .cameraContent .videoContainer{width:calc(100% - 100px);float:left;position:relative}#camera .cameraContent .videoContainer canvas#additional,#camera .cameraContent .videoContainer canvas#frame,#camera .cameraContent .videoContainer canvas#snapImage,#camera .cameraContent .videoContainer video{width:100%;max-width:100vw;max-height:calc(100vh - 130px);float:left}#camera .cameraContent .videoContainer canvas#frame,#camera .cameraContent .videoContainer canvas#snapImage{display:none}#camera .cameraContent .videoContainer canvas#additional{display:block;position:absolute;top:0;left:0}#camera .cameraContent .filters{width:100px;float:left}#camera .cameraContent .filters label input{display:none}#camera .cameraContent .filters label input:checked+span{background:#0052cc;color:#fff}#camera .cameraContent .filters label span.filter{width:70px;height:20px;margin:0 15px 5px;cursor:pointer;background:#eee;line-height:20px;text-align:center;font-size:13px;float:left}#camera .cameraContent #cameraButtons{float:left;width:100%;height:50px;line-height:50px}#camera .cameraContent #cameraButtons button{min-width:50px;border:none;display:none;cursor:pointer}#camera .cameraContent #cameraButtons button *{cursor:pointer}#camera .cameraContent #cameraButtons button#snap{display:inline-block}#camera .cameraContent #cameraButtons button#applySnap{background:#129a01;color:#fff}#camera .cameraContent #cameraButtons button#cancelSnap{background:#c50000;color:#fff}#camera .cameraContent #cameraButtons button#saveSnapOnComputer{background:#0052cc;color:#fff}#camera.snapped #cameraButtons button{display:inline-block}#camera.snapped #cameraButtons button#snap{display:none}#camera.snapped .cameraContent .videoContainer canvas#additional,#camera.snapped .cameraContent .videoContainer video#video{display:none}#camera.snapped .cameraContent .videoContainer canvas#snapImage{display:block}menu#contextmenu{display:none;width:230px;height:auto;position:fixed;top:50%;left:50%;background:#fff;z-index:10}menu#contextmenu ul li{cursor:pointer;float:left;height:30px;line-height:30px;position:relative;width:100%;background:#fff;color:#000}menu#contextmenu ul li *{cursor:pointer}menu#contextmenu ul li .content{width:100%;float:left}menu#contextmenu ul li .content span{height:inherit;float:left}menu#contextmenu ul li .content span.icon{width:20px}menu#contextmenu ul li .content span.icon i{float:left;width:20px;height:30px;line-height:30px}menu#contextmenu ul li .content span.text{width:calc(100% - 130px)}menu#contextmenu ul li .content span.shortcut{text-align:right;text-transform:uppercase;width:70px;font-size:11px;color:#2e3436}menu#contextmenu ul li .content span.arrow{color:#2e3436;width:10px;margin:0 5px;font-size:11px;visibility:hidden;opacity:0}menu#contextmenu ul li .submenu{width:230px;position:absolute;display:none;top:0;left:230px}menu#contextmenu ul li .submenu+.content span.arrow.left-arrow{visibility:hidden;opacity:0}menu#contextmenu ul li .submenu+.content span.arrow.right-arrow{visibility:visible;opacity:1;float:right}menu#contextmenu ul li.disabled{color:#888}menu#contextmenu ul li:not(.disabled):hover{color:#0052cc;background-color:#dedede}menu#contextmenu ul li:not(.disabled):hover .submenu{display:block;color:#000}menu#contextmenu.left-submenu ul li .submenu{left:-230px}menu#contextmenu.left-submenu ul li .submenu+.content span.arrow.left-arrow{visibility:visible;opacity:1}menu#contextmenu.left-submenu ul li .submenu+.content span.arrow.right-arrow{visibility:hidden;opacity:0}*{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;cursor:default;font-family:'Open Sans',sans-serif;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;margin:0;padding:0}ul{list-style:none}html body{top:0;left:0;width:100%;height:100%;float:left;position:fixed;overflow:hidden}html body #content{float:left;background:#373636;overflow:auto;width:calc(100vw - 200px);height:calc(100vh - 20px)}html body #content #drawHandler{border:1px dotted #0052cc;position:relative;overflow:hidden}html body #content #drawHandler #draw,html body #content #drawHandler #drawHandler,html body #content #drawHandler #eventHandler{width:600px;height:400px;overflow:hidden;position:relative}html body #content #drawHandler #eventHandler{z-index:3;position:absolute}html body #content #drawHandler #draw canvas{position:absolute;cursor:crosshair;border:1px dotted #0052cc}html body #content #drawHandler #draw canvas.invisible{visibility:hidden}html body #eraseRect,html body #prepareCircle,html body #prepareRect,html body #selectRectangle{display:none;position:absolute;z-index:2;border:1px solid #000}html body #selectRectangle{cursor:move}html body #eraseRect{border-radius:100%;border-color:#ccc} \ No newline at end of file diff --git a/build/js/built.js b/build/js/built.js deleted file mode 100644 index e69e81a..0000000 --- a/build/js/built.js +++ /dev/null @@ -1,2049 +0,0 @@ -class Camera { - constructor() { - this.cameraStream = null; - this.filters = { - 'sepia': false, - 'noise': false, - 'greyscale': false, - 'negative': false - }; - let $this = this; - this.filterInterval = null; - $.get('parts/camera.part.html', function (data) { - $('body').append('
'); - - $('#camera').html(data); - - $('#snap').click($this.snap); - $('#saveSnapOnComputer').click($this.saveOnComputer); - $('#applySnap').click($this.applySnap); - $('#cancelSnap').click($this.cancelSnap); - $('#switchFullscreen').click(function (e) { - $('#camera').toggleClass('fullscreen'); - }) - $('#closeCamera').click(function () { - $this.stop(); - }); - $('.filter-checkbox').change(function (e) { - let filterName = $(this).parent().find('.filter').data('id'); - $this.filters[filterName] = $(this).is(':checked'); - }); - }); - } - - init() { - let $this = this; - $('#camera').addClass('opened'); - let video = document.getElementById('video'); - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) { - video.src = window.URL.createObjectURL(stream); - video.play(); - $this.cameraStream = stream; - $('#camera #cameraTitle').text($this.cameraStream.getTracks()[0].label); - $this.setFilter(); - }); - } - } - - stop() { - let $this = this; - let track = $this.cameraStream.getTracks()[0]; - track.stop(); - $this.cameraStream = null; - $('#camera').removeClass('opened'); - } - - snap() { - let snapImage = document.getElementById('snapImage'); - let snapImageContext = snapImage.getContext('2d'); - let video = document.getElementById('video'); - let additionalLayer = document.getElementById('additional'); - let additionalLayerContext = additionalLayer.getContext('2d'); - let videoImageData = additionalLayerContext.getImageData(0, 0, additionalLayer.width, additionalLayer.height); - snapImageContext.putImageData(videoImageData, 0, 0); - $('#camera').addClass('snapped'); - } - - saveOnComputer() { - file.downloadFromCamera('snapImage'); - } - - cancelSnap() { - let snapImage = document.getElementById('snapImage'); - let snapImageContext = snapImage.getContext('2d'); - snapImageContext.clearRect(0, 0, snapImage.width, snapImage.height); - $('#camera').removeClass('snapped'); - if (this.filterInterval) { - clearInterval(this.filterInterval); - } - } - - applySnap() { - let snapImage = document.getElementById('snapImage'); - layers.newLayer(); - layers.setLayerSize(layers.activeId, snapImage.width, snapImage.height); - webDraft.positionElements(); - ctx.drawImage(snapImage, 0, 0); - layers.saveState(); - $('#camera').removeClass('snapped'); - } - - setFilter() { - let additionalLayer = document.getElementById('additional'); - let additionalLayerContext = additionalLayer.getContext('2d'); - let frame = document.getElementById('frame'); - let frameContext = frame.getContext('2d'); - let video = document.getElementById('video'); - let videoImageData = additionalLayerContext.getImageData(0, 0, additionalLayer.width, additionalLayer.height); - if (this.filterInterval) { - clearInterval(this.filterInterval); - } - let $this = this; - setInterval(function (e) { - frameContext.drawImage(video, 0, 0, video.width, video.height); - videoImageData = frameContext.getImageData(0, 0, video.width, video.height); - - for (let i in $this.filters) { - if ($this.filters[i]) { - videoImageData = $this[i + 'Filter'](videoImageData); - } - } - - additionalLayerContext.putImageData(videoImageData, 0, 0); - }, 1) - } - - negativeFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - pixels[i] = 255 - pixels[i]; // red - pixels[i + 1] = 255 - pixels[i + 1]; // green - pixels[i + 2] = 255 - pixels[i + 2]; // blue - } - - return imageData; - } - - greyscaleFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let r = pixels[i]; - let g = pixels[i + 1]; - let b = pixels[i + 2]; - let avg = 0.3 * r + 0.59 * g + 0.11 * b; - - pixels[i] = avg; // red - pixels[i + 1] = avg; // green - pixels[i + 2] = avg; // blue - } - - return imageData; - } - - noiseFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let rand = (0.5 - Math.random()) * 100; - pixels[i] += rand; // red - pixels[i + 1] += rand; // green - pixels[i + 2] += rand; // blue - } - - return imageData; - } - - sepiaFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let r = pixels[i]; - let g = pixels[i + 1]; - let b = pixels[i + 2]; - let avg = 0.3 * r + 0.59 * g + 0.11 * b; - - pixels[i] = avg + 100; // red - pixels[i + 1] = avg + 50; // green - pixels[i + 2] = avg; // blue - } - - return imageData; - } -} - -class Contextmenu { - constructor(items) { - this.items = items; - let $this = this; - $('body').append(''); - $('#contextmenu').html('
    '); - for (let i in items) { - let li = this.buildItem(this.items[i]); - - $('#contextmenu ul').append(li); - - if (this.items[i].onclick) { - $('#contextmenu ul li').last().click(function (e) { - $this.items[i].onclick(); - $this.hide(e); - }); - } - - if (this.items[i].submenu && this.items[i].submenu.length > 0) { - $('#contextmenu ul li').last().prepend(''); - for (let j in this.items[i].submenu) { - - let li = this.buildItem(this.items[i].submenu[j]); - - $('#contextmenu ul li ul.submenu').append(li); - - if (this.items[i].submenu[j].onclick) { - $('#contextmenu ul li').last().click(function (e) { - $this.items[i].submenu[j].onclick(); - $this.hide(e); - }); - } - } - } - } - - $('body').contextmenu(function (e) { - e.preventDefault(); - $this.show(e); - }).click(function (e) { - if (!$('#contextmenu').is(":hover")) { - $this.hide(e); - } - }); - } - show(e) { - let t = e.pageY; - let l = e.pageX; - $('#contextmenu').removeClass('left-submenu'); - if (t + $('#contextmenu').height() > $(window).height()) { - t = $(window).height() - $('#contextmenu').height(); - } - if (l + $('#contextmenu').width() > $(window).width()) { - l = $(window).width() - $('#contextmenu').width(); - } - if (l + ($('#contextmenu').width() * 2) > $(window).width()) { - $('#contextmenu').addClass('left-submenu'); - } - $('#contextmenu').css({ - top: t + "px", - left: l + "px" - }).show(); - } - hide(e) { - $('#contextmenu').hide(); - } - buildItem(item) { - let arrowLeft = ''; - let arrowRight = ''; - let shortcut = ''; - let text = ''; - let icon = '' + arrowLeft + icon + text + shortcut + arrowRight + ''; - } -} -class Draw { - drawStyle() { - ctx.lineWidth = webDraft.size; - - if (webDraft.selectedTool === RECTANGLE) { - ctx.lineJoin = ctx.lineCap = 'miter'; - } else { - ctx.lineJoin = ctx.lineCap = 'round'; - } - - if (shapes.fill.isSet === true) { - ctx.fillStyle = hexToRgba(shapes.fill.color, shapes.fill.opacity); - } else { - ctx.fillStyle = "transparent"; - } - - if (webDraft.selectedTool === ERASER) { - ctx.globalCompositeOperation = "destination-out"; - } else { - ctx.globalCompositeOperation = "source-over"; - } - - if (webDraft.shadow.isShadow === true && webDraft.selectedTool !== ERASER) { - ctx.shadowBlur = webDraft.shadow.blur; - ctx.shadowColor = webDraft.shadow.color; - ctx.shadowOffsetX = webDraft.shadow.offsetX; - ctx.shadowOffsetY = webDraft.shadow.offsetY; - } else { - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } - - ctx.strokeStyle = webDraft.color;//line color - } - drawing() { - ctx.beginPath(); - ctx.moveTo(webDraft.mPosition.x, webDraft.mPosition.y); - this.drawStyle(); - ctx.lineTo(webDraft.mPosition.x, webDraft.mPosition.y); - ctx.stroke(); - } - drawWeb() { - ctx.beginPath(); - this.drawStyle(); - ctx.moveTo(points[layers.activeId][points[layers.activeId].length - 2].x, points[layers.activeId][points[layers.activeId].length - 2].y); - ctx.lineTo(points[layers.activeId][points[layers.activeId].length - 1].x, points[layers.activeId][points[layers.activeId].length - 1].y); - ctx.stroke(); - let dx, dy, d; - let len = points[layers.activeId].length; - for (let i = 0; i < len; i++) { - dx = points[layers.activeId][i].x - points[layers.activeId][points[layers.activeId].length - 1].x; - dy = points[layers.activeId][i].y - points[layers.activeId][points[layers.activeId].length - 1].y; - d = dx * dx + dy * dy; - if (d < webDraft.sensitivityPoints) { - ctx.beginPath(); - this.drawStyle(); - ctx.moveTo(points[layers.activeId][points[layers.activeId].length - 1].x + (dx * 0.2), points[layers.activeId][points[layers.activeId].length - 1].y + (dy * 0.2)); - ctx.lineTo(points[layers.activeId][i].x - (dx * 0.2), points[layers.activeId][i].y - (dy * 0.2)); - ctx.stroke(); - } - } - } -} -var events = { - color: function () { - $('#generalColor').colorpicker({ - onSelect: function (color) { - $('#generalColor .color').css({ - 'background': color.hex - }); - webDraft.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - $('#shadowColor').colorpicker({ - onSelect: function (color) { - $('#shadowColor .color').css({ - 'background': color.hex - }); - webDraft.shadow.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - $('#fillColor').colorpicker({ - onSelect: function (color) { - $('#fillColor .color').css({ - 'background': color.hex - }); - shapes.fill.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - }, - info: function () { - $(".close-info").click(function () { - $("#info").hide(); - }); - }, - buttons: function () { - $("input[type=checkbox]#isShadow").change(function () { - webDraft.shadow.isShadow = $(this).is(":checked");//return true if is :checked or false if not - - if (webDraft.shadow.isShadow) { - $("#shadowColor, #shadow_slider, #shadowSquare").show(); - } else { - $("#shadowColor, #shadow_slider, #shadowSquare").hide(); - } - }); - $("input[type=checkbox]#isFillSet").change(function () { - shapes.fill.isSet = $(this).is(":checked");//return true if is :checked or false if not - - if (shapes.fill.isSet) { - $("#fillColor, #fillOpacity_slider").show(); - } else { - $("#fillColor, #fillOpacity_slider").hide(); - } - }); - - $(".paintTool").click(function () { - $(".paintTool").removeClass("active"); - $(this).addClass("active"); - - var thisId = $(this).attr("id"); - - webDraft.selectedTool = thisId; - - if (thisId === COLORSAMPLER) { - $("#previewColorSampler").show(); - } else { - $("#previewColorSampler").hide(); - } - - if (thisId === WEB) { - $("#sensitivityPoints_slider").show(); - } else { - $("#sensitivityPoints_slider").hide(); - } - - if (thisId === ERASER) { - $("#eraseRect").show(); - $("#draw, #drawHandler, #eventHandler").css({"cursor": "none"}); - } else { - $("#eraseRect").hide(); - $("#draw, #drawHandler, #eventHandler").css({"cursor": "default"}); - } - - if (thisId !== RECTANGLE) { - $("#prepareRect").hide(); - } - - if (thisId !== CIRCLE) { - $("#prepareCircle").hide(); - } - - if (thisId !== SELECT) { - $("#selectRectangle").hide(); - $("#hint, .hintGroup#selecting").hide(); - } else { - $("#hint, .hintGroup#selecting").show(); - } - - if (thisId !== TEXT) { - $("#textRectangle, #textOptions").hide(); - } - - if (thisId === RECTANGLE || thisId === CIRCLE) { - $("#fillShapeInput").show(); - } else { - $("#fillShapeInput").hide(); - $("input#isFillSet").attr('checked', false).change(); - } - }); - }, - sliders: function () { - //changing size - $("input[type=range]#pointSize").on('mousemove touchmove', function () { - webDraft.size = $(this).val(); - $("#pointSizeValue").text("size:" + $(this).val() + "px"); - }); - //changing shadow blur - $("input[type=range]#ShadowBlur").on('mousemove touchmove', function () { - webDraft.shadow.blur = $(this).val(); - $("#ShadowBlurValue").text("blur:" + $(this).val() + "px"); - }); - //changing sensitivity of web points - $("input[type=range]#sensitivityPoints").on('mousemove touchmove', function () { - webDraft.sensitivityPoints = $(this).val(); - $("#sensitivityPointsValue").text("sensitivity:" + Math.floor($(this).val() / 100) + "%"); - }); - //changing fill opacity - $("input[type=range]#fillOpacity").on('mousemove touchmove', function () { - shapes.fill.opacity = $(this).val(); - $("#fillOpacityValue").text("fill opacity:" + Math.floor($(this).val()) + "%"); - }); - } - -}; -class File { - constructor() { - $('body').append(''); - } - - upload(event) { - let input = event.target; - let widthImg; - let heightImg; - let output = new Image(); - - output.src = URL.createObjectURL(input.files[0]); - - output.onload = function () { - widthImg = this.width; - heightImg = this.height; - - layers.newLayer(); - layers.setLayerSize(layers.activeId, widthImg, heightImg); - webDraft.positionElements(); - - ctx.drawImage(output, 0, 0); - - layers.saveState(); - keys.O = false; - }; - } - download() { - $(webDraft.draw.selectorId).append(''); - - let temp_c = document.getElementById("tmpCanvas"); - let temp_ctx = temp_c.getContext("2d"); - $("#tmpCanvas").width(webDraft.draw.width).height(webDraft.draw.height); - - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string" && layers.list[i].visible === true) { - let imgData = document.getElementById(layers.list[i].id); - let top = parseInt($("#" + layers.list[i].id).css("top")); - let left = parseInt($("#" + layers.list[i].id).css("left")); - temp_ctx.drawImage(imgData, top, left); - } - } - - temp_c.toBlob(function (blob) { - saveAs(blob, "WebDraft-image.png"); - }); - - $("#tmpCanvas").remove(); - } - downloadFromCamera(id = null) { - let imgData = document.getElementById(id); - $(webDraft.draw.selectorId).append(''); - - let temp_c = document.getElementById("tmpCanvas"); - let temp_ctx = temp_c.getContext("2d"); - $("#tmpCanvas").width(imgData.width).height(imgData.height); - - temp_ctx.drawImage(imgData, 0, 0); - - temp_c.toBlob(function (blob) { - saveAs(blob, "WebDraft-camera-photo.png"); - }); - - $("#tmpCanvas").remove(); - - } -} -var keys = { - Enter: false, //press Enter - Esc: false, //press Escape (Esc) - f11: false, //press F11 - f12: false, //press F12 - delete: false, //press delete - C: false, - X: false, - V: false, - O: false, - I: false -}; - -$(document) - .keydown(function (event) { - if (webDraft.selectedTool !== TEXT && $('#resizer').is('hidden')) { - event.preventDefault(); - } - - switch (event.keyCode) { - case 13 : - keys.Enter = true; - break; - case 27 : - keys.Esc = true; - break; - case 46 : - keys.delete = true; - break; - case 67 : - keys.C = true; - break; - case 73 : - keys.I = true; - break; - case 79 : - keys.O = true; - break; - case 83 : - keys.S = true; - break; - case 86 : - keys.V = true; - break; - case 88 : - keys.X = true; - break; - case 122 : - keys.f11 = true; - break; - case 123 : - keys.f12 = true; - break; - } - if (keys.delete) { - if (webDraft.selectedTool === SELECT) { - select.delSelectedPart(); - } - if (event.ctrlKey) { - webDraft.clear(); - } - } - - if (keys.f12 || keys.f11 || event.ctrlKey || keys.delete) { - event.preventDefault(); - } - - if (keys.C) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.copySelectedPart(); - } - } - - if (keys.I) { - if (event.ctrlKey) { - $("#info").toggle(); - } - } - - if (keys.X) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.cutSelectedPart(); - } - } - - if (keys.V) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.pasteSelectedPart(); - } - } - if (keys.O) { - if (event.ctrlKey) { - $("#fileUploader").click(); - } - } - if (keys.S) { - if (event.ctrlKey) { - file.download(); - } - } - - }) - .keyup(function (event) { - switch (event.keyCode) { - case 13 : - keys.Enter = false; - break; - case 27 : - keys.Esc = false; - break; - case 46 : - keys.delete = false; - break; - case 67 : - keys.C = false; - break; - case 73 : - keys.I = false; - break; - case 79 : - keys.O = false; - break; - case 83 : - keys.S = false; - break; - case 86 : - keys.V = false; - break; - case 88 : - keys.X = false; - break; - case 122 : - keys.f11 = false; - break; - case 123 : - keys.f12 = false; - break; - } - }); - -$(window).bind('mousewheel DOMMouseScroll', function (event) { - if (event.ctrlKey === true) { - event.preventDefault(); - - if (event.originalEvent.wheelDelta / 120 > 0) { - if (webDraft.size < 250) { - webDraft.size += 2; - } else { - webDraft.size = 250; - } - } else { - if (webDraft.size > 1) { - webDraft.size -= 2; - } else { - webDraft.size = 1; - } - } - $("#eraseRect").css({ - "width": webDraft.size, - "height": webDraft.size, - "top": event.pageY - (webDraft.size / 2) + "px", - "left": event.pageX - (webDraft.size / 2) + "px" - }); - $("input#pointSize").val(webDraft.size); - $("#pointSizeValue").text("size:" + webDraft.size + "px"); - } - - if (event.altKey === true) { - // event.preventDefault(); - } -}); -class Layers { - constructor() { - $.get('parts/layers.part.html', function (data) { - $('#layers').html(data); - layers.newLayer(); - }); - - this.activeId = ""; - this.list = []; - } - setLayerPosition(layerId, top, left) { - $("canvas#" + layerId).css({ - "top": top, - "left": left - }); - } - setLayerSize(layerId, width, height) { - let image = { - id: new Array(), - img: new Array() - }; - - let active = layers.activeId; - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - image.img[i] = ctx.getImageData(0, 0, webDraft.draw.width, webDraft.draw.height); - image.id[i] = layers.list[i].id; - } - } - - if (layerId === "") { - webDraft.draw.width = width; - webDraft.draw.height = height; - $("canvas.canvas-draw").attr({ - "width": webDraft.draw.width, - "height": webDraft.draw.height - }); - } else { - $("canvas#" + layerId).attr({ - "width": width, - "height": height - }); - webDraft.draw.width = 0; - webDraft.draw.height = 0; - $("canvas.canvas-draw").each(function () { - let w = parseInt($(this).attr("width")); - let h = parseInt($(this).attr("height")); - if (webDraft.draw.width < w) - webDraft.draw.width = w; - - if (webDraft.draw.height < h) - webDraft.draw.height = h; - }); - } - - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - ctx.putImageData(image.img[i], 0, 0); - layers.saveState(); - } - } - - if (active !== "") - layers.select(active); - } - saveState() { - let imgSrc = document.getElementById(layers.activeId).toDataURL(); - $(".layerView[data-id=" + layers.activeId + "]").find("img").attr("src", imgSrc).show(); - } - newLayer() { - let j; - if (isNaN(parseInt($(".layerView:last").attr("id")))) { - j = 0; - } else { - j = parseInt($(".layerView:last").attr("id")) + 1; - } - - let countViews = $("#listLayers").children(".layerView").length; - - if (countViews < 15) { - randomId = webDraft.makeid(); - - $(webDraft.draw.selectorId).append(''); - $("#listLayers").append(` -
    -
    - -
    -
    - -
    ` - ); - - let new_layer = { - visible: true, - id: randomId, - top: 0, - left: 0 - }; - layers.list.push(new_layer); - - $(".layerView").click(function () { - if (!$(this).hasClass("hidden")) { - $(".layerView").removeClass("active"); - $(this).addClass("active"); - - let identifier = $(this).attr("data-id"); - - layers.select(identifier); - } - }); - $(".hideLayer").click(function () { - let nr = $(this).parent(".layerView").attr("id"); - - $(this).hide(); - $(this) - .parent(".layerView") - .find(".showLayer") - .css({"display": "block"}); - - layers.hide(nr); - }); - $(".showLayer").click(function () { - let nr = $(this).parent(".layerView").attr("id"); - - $(this).hide(); - $(this) - .parent(".layerView") - .find(".hideLayer") - .css({"display": "block"}); - - layers.show(nr); - }); - $(".layerView[data-id=" + randomId + "]").click(); - points[randomId] = []; - - } - } - delete() { - let identifier = $(".layerView.active").attr("data-id"); - let nr = $(".layerView.active").attr("id"); - - let countViews = $(".layerView").length; - - if (countViews > 1) { - let i = parseInt(nr); - $(".layerView#" + i).remove(); - $("canvas#" + identifier).remove(); - for (i in layers.list) { - if (!$('canvas#' + layers.list[i].id).length) { - layers.list.splice(i, 1); - } - } - - let j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - - webDraft.draw.width = 0; - webDraft.draw.height = 0; - $("canvas.canvas-draw").each(function () { - const w = parseInt($(this).attr("width")); - const h = parseInt($(this).attr("height")); - if (webDraft.draw.width < w) - webDraft.draw.width = w; - - if (webDraft.draw.height < h) - webDraft.draw.height = h; - }); - $(".layerView").first().click(); - webDraft.positionElements(); - } - } - hide(nr) { - let i = parseInt(nr); - - $(".layerView#" + i).addClass("hidden"); - $("canvas#" + layers.list[i].id).addClass("invisible"); - - let j = 0; - $("canvas.canvas-draw").not('#snapImage').each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - // $(".layerView#" + i).next().not(".hidden").click() - } - show(nr) { - let i = parseInt(nr); - - $(".layerView#" + i).removeClass("hidden"); - $("canvas#" + layers.list[i].id).removeClass("invisible"); - - let j = 0; - $("canvas.canvas-draw").not('#snapImage').each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - } - select(id) { - if (!$(".layerView[data-id=" + id + "]").hasClass("hidden")) { - canvas = document.getElementById(id); - ctx = canvas.getContext('2d'); - - layers.activeId = id; - } - } - rotate(angle) { - let image = new Image(); - image.src = canvas.toDataURL(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(angle * Math.PI / 180); - ctx.drawImage(image, -image.width / 2, -image.width / 2); - ctx.restore(); - layers.saveState(); - } - mirror(direction) { - if (direction === 'horizontal' || direction === 'vertical') { - let image = new Image(); - image.src = canvas.toDataURL(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.translate(canvas.width / 2, canvas.height / 2); - if (direction === 'vertical') - ctx.scale(-1, 1); - else if (direction === 'horizontal') - ctx.scale(1, -1); - ctx.drawImage(image, -image.width / 2, -image.height / 2); - ctx.restore(); - layers.saveState(); - } - } - negative() { - let destX = 0; - let destY = 0; - - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - pixels[i] = 255 - pixels[i]; // red - pixels[i + 1] = 255 - pixels[i + 1]; // green - pixels[i + 2] = 255 - pixels[i + 2]; // blue - // i+3 is alpha (the fourth element) - } - - // overwrite original image - ctx.putImageData(imageData, 0, 0);//add the function call in the imageObj.onload - layers.saveState(); - } - moveUp() { - let firstID = $(".layerView").first().attr("data-id"); - if (firstID !== layers.activeId) { - let thisNr = $(".layerView[data-id=" + layers.activeId + "]").attr("id"); - let prevNr = parseInt(thisNr) - 1; - let prevId = $(".layerView#" + prevNr).attr("data-id"); - let thisLayer = $(".layerView#" + thisNr); - let prevLayer = $(".layerView#" + prevNr); - - thisLayer.insertBefore(".layerView#" + prevNr); - thisLayer.removeAttr("id").attr("id", prevNr); - prevLayer.removeAttr("id").attr("id", thisNr); - - $("canvas#" + layers.activeId).insertBefore("canvas#" + prevId); - - let j = 0; - $("canvas.canvas-draw").each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - return true; - } - return false; - } - moveDown() { - let lastID = $(".layerView").last().attr("data-id"); - if (lastID !== layers.activeId) { - let thisNr = $(".layerView[data-id=" + layers.activeId + "]").attr("id"); - let nextNr = parseInt(thisNr) + 1; - let nextId = $(".layerView#" + nextNr).attr("data-id"); - let thisLayer = $(".layerView#" + thisNr); - let prevLayer = $(".layerView#" + nextNr); - - thisLayer.insertAfter(".layerView#" + nextNr); - thisLayer.removeAttr("id").attr("id", nextNr); - prevLayer.removeAttr("id").attr("id", thisNr); - - $("canvas#" + layers.activeId).insertAfter("canvas#" + nextId); - let j = 0; - $("canvas.canvas-draw").each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - return true; - } - return false; - } -} -class Resizer { - constructor() { - let $this = this; - $.get('parts/resizer.part.html', function (data) { - $('#resizer').html(data); - - $("#resizer input[type=number]").keyup(function (e) { - $this.onkeyup(e); - }); - }); - } - show() { - $("#resizer").show(); - $("input[type=number]#drawWidth").val(webDraft.draw.width); - $("input[type=number]#drawHeight").val(webDraft.draw.height); - $("#resizeinfo").html(webDraft.draw.width + " " + webDraft.draw.height); - } - cancel() { - $("#resizer").hide(); - $("input[type=number]#drawWidth").val(webDraft.draw.width); - $("input[type=number]#drawHeight").val(webDraft.draw.height); - } - apply() { - $("#resizer").hide(); - let w, h; - if ($("#allLayersResizing").is(":checked")) { - w = $("input[type=number]#drawWidth").val(); - h = $("input[type=number]#drawHeight").val(); - - layers.setLayerSize('', w, h); - } else { - w = $("input[type=number]#drawWidth").val(); - h = $("input[type=number]#drawHeight").val(); - - layers.setLayerSize(layers.activeId, w, h); - } - webDraft.positionElements(); - } - onkeyup(e) { - this.onchange(); - - if (e.keyCode === 13) { - $("#apply").click(); - } - } - onchange() { - let xSize = parseInt($("input[type=number]#drawWidth").val()); - let ySize = parseInt($("input[type=number]#drawHeight").val()); - $("#resizeinfo").html(xSize + " " + ySize); - } -} -class Select { - constructor() { - $('#drawHandler').append('
    '); - this.hoverSelectRectangle = false; - this.isSelecting = false; - this.startSelectPoints = { - x: 0, - y: 0 - }; - } - initSelect() { - this.startSelectPoints = { - x: webDraft.mPosition.x, - y: webDraft.mPosition.y - }; - } - startSelect() { - let x, y, width, height; - if (this.startSelectPoints.x <= webDraft.mPosition.x) { - x = this.startSelectPoints.x; - width = webDraft.mPosition.x - this.startSelectPoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startSelectPoints.x - webDraft.mPosition.x; - } - if (this.startSelectPoints.y <= webDraft.mPosition.y) { - y = this.startSelectPoints.y; - height = webDraft.mPosition.y - this.startSelectPoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startSelectPoints.y - webDraft.mPosition.y; - } - $("#selectRectangle") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "width": width + "px", - "height": height + "px", - "border": "1px dashed #fff", - "background": "transparent" - }); - this.isSelecting = true; - } - selectOpt() { - this.isSelecting = false; - } - delSelectedPart() { - if ($("#selectRectangle").css("background-image") === "none") {//if selectRectangle is empty then clear part of image hovered by it - xpos = parseInt($("#selectRectangle").css("left")); - ypos = parseInt($("#selectRectangle").css("top")); - ctx.clearRect(xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height()); - } else { //else clear only selectRectangle background - $("#selectRectangle").css({"background": "transparent"}); - } - - layers.saveState(); - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px", - "background": "transparent" - }) - .width(0) - .height(0) - .hide(); - } - copySelectedPart() { - if ($("#selectRectangle").css("background-image") === "none") { - $(webDraft.draw.selectorId).append(''); - - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var testC = document.getElementById("tmpCanvas"); - var testCtx = testC.getContext('2d'); - - testCtx.drawImage(canvas, xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height(), 0, 0, $("#selectRectangle").width(), $("#selectRectangle").height()); - - var bgImg = testC.toDataURL(); - - $("#tmpCanvas").remove(); - $("#selectRectangle").css({"background": "url(" + bgImg + ") -1px -1px no-repeat"}); - } - } - cutSelectedPart() { - $(webDraft.draw.selectorId).append(''); - - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var testC = document.getElementById("tmpCanvas"); - var testCtx = testC.getContext('2d'); - - testCtx.drawImage(canvas, xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height(), 0, 0, $("#selectRectangle").width(), $("#selectRectangle").height()); - - var bgImg = testC.toDataURL(); - - $("#tmpCanvas").remove(); - $("#selectRectangle").css({"background": "url(" + bgImg + ") -1px -1px no-repeat "}); - - ctx.clearRect(xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height()); - layers.saveState(); - } - pasteSelectedPart() { - if ($("#selectRectangle").css("background-image") !== "none") { - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var img = new Image(); - var bg = $("#selectRectangle") - .css("background-image") - .replace('url(', '') - .replace(')', '') - .replace('"', '') - .replace('"', ''); - img.onload = function () { - ctx.drawImage(img, xpos, ypos); //save part of image when loaded - - layers.saveState(); //and then update layer preview - }; - img.src = bg; - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px", - "background": "transparent" - }) - .width(0) - .height(0) - .hide(); - } - } -} -; -class Shapes { - constructor() { - $('#drawHandler').append(` -
    -
    `); - this.startShapePoints = { - x: 0, - y: 0 - }; - this.fill = { - isSet: false, - color: "#ffffff", - opacity: 100 - }; - } - - startShape() { - this.startShapePoints.x = webDraft.mPosition.x; - this.startShapePoints.y = webDraft.mPosition.y; - } - prepareRect() { - let x, y, width, height; - if (this.startShapePoints.x <= webDraft.mPosition.x) { - x = this.startShapePoints.x; - width = webDraft.mPosition.x - this.startShapePoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startShapePoints.x - webDraft.mPosition.x; - } - if (this.startShapePoints.y <= webDraft.mPosition.y) { - y = this.startShapePoints.y; - height = webDraft.mPosition.y - this.startShapePoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startShapePoints.y - webDraft.mPosition.y; - } - $("#prepareRect") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "width": width + "px", - "height": height + "px", - "border": webDraft.size + "px solid " + webDraft.color - }); - if (this.fill.isSet) { - $("#prepareRect").css({"background": hexToRgba(this.fill.color, this.fill.opacity)}); - } else { - $("#prepareRect").css({"background": "transparent"}); - } - if (webDraft.shadow.isShadow) { - $("#prepareRect").css({"box-shadow": webDraft.shadow.offsetX + "px " + webDraft.shadow.offsetY + "px " + webDraft.shadow.blur + "px " + webDraft.shadow.color}); - } else { - $("#prepareRect").css({"box-shadow": "none"}); - } - } - drawRect() { - let x, y, width, height; - if (this.startShapePoints.x >= 0 && this.startShapePoints.y >= 0) { - if (this.startShapePoints.x <= webDraft.mPosition.x) { - x = this.startShapePoints.x; - width = webDraft.mPosition.x - this.startShapePoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startShapePoints.x - webDraft.mPosition.x; - } - if (this.startShapePoints.y <= webDraft.mPosition.y) { - y = this.startShapePoints.y; - height = webDraft.mPosition.y - this.startShapePoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startShapePoints.y - webDraft.mPosition.y; - } - $("#prepareRect").hide(); - ctx.beginPath(); - draw.drawStyle(); - ctx.rect(x, y, width, height); - ctx.fill(); - ctx.stroke(); - } - this.startShapePoints.x = -1; - this.startShapePoints.y = -1; - } - prepareCircle() { - let x, y, width, height, radius; - x = this.startShapePoints.x; - y = this.startShapePoints.y; - - if (x <= webDraft.mPosition.x) { - width = webDraft.mPosition.x - x; - } else { - width = x - webDraft.mPosition.x; - } - - if (y <= webDraft.mPosition.y) { - height = webDraft.mPosition.y - y; - } else { - height = y - webDraft.mPosition.y; - } - - if (width > height) { - radius = width; - } else { - radius = height; - } - $("#prepareCircle") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) - radius + "px", - "left": x + parseInt($(canvas).css('left')) - radius + "px", - "width": radius * 2 + "px", - "height": radius * 2 + "px", - "border": webDraft.size + "px solid " + webDraft.color, - "border-radius": "100%" - }); - - if (this.fill.isSet) { - $("#prepareCircle").css({"background": hexToRgba(this.fill.color, this.fill.opacity)}); - } else { - $("#prepareCircle").css({"background": "transparent"}); - } - - if (webDraft.shadow.isShadow) { - $("#prepareCircle").css({"box-shadow": webDraft.shadow.offsetX + "px " + webDraft.shadow.offsetY + "px " + webDraft.shadow.blur + "px " + webDraft.shadow.color}); - } else { - $("#prepareCircle").css({"box-shadow": "none"}); - } - } - drawCircle() { - let x, y, width, height, radius; - x = this.startShapePoints.x; - y = this.startShapePoints.y; - - if (x >= 0 && y >= 0) { - - if (x <= webDraft.mPosition.x) { - width = webDraft.mPosition.x - x; - } else { - width = x - webDraft.mPosition.x; - } - if (y <= webDraft.mPosition.y) { - height = webDraft.mPosition.y - y; - } else { - height = y - webDraft.mPosition.y; - } - if (width > height) { - radius = width; - } else { - radius = height; - } - $("#prepareCircle").hide(); - - ctx.beginPath(); - draw.drawStyle(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.fill(); - ctx.stroke(); - } - this.startShapePoints.x = -1; - this.startShapePoints.y = -1; - } -} -class Text { - constructor() { - $('#drawHandler').append('
    '); - - $.get('parts/text-options.part.html', function (data) { - $('#textOptions').html(data); - - - $("#selectFontType").change(function () { - $("#textRectangle").css("font-family", $(this).val()); - }); - $("#selectFontSize").change(function () { - $("#textRectangle").css("font-size", $(this).val() + "px"); - }); - $(".textPostionTool").click(function () { - $(".textPostionTool").removeClass("active"); - $(this).addClass("active"); - $("#textRectangle").css("text-align", $(this).attr('id')); - }); - $(".styleTool").click(function () { - $(this).toggleClass("active"); - switch ($(this).attr("id")) { - case 'bold': - if ($(this).hasClass('active')) - $("#textRectangle").css("font-weight", 'bold'); - else - $("#textRectangle").css("font-weight", 'normal'); - break; - case 'italic': - if ($(this).hasClass('active')) - $("#textRectangle").css("font-style", 'italic'); - else - $("#textRectangle").css("font-style", 'normal'); - break; - case 'underline': - case 'line-through': - var textDecoration = ""; - $(".textDecoration.active").each(function () { - textDecoration += $(this).attr("id") + " "; - }); - if ($(".textDecoration.active").length > 0) - $("#textRectangle").css("text-decoration", textDecoration); - else - $("#textRectangle").css("text-decoration", 'none'); - break; - default: - - } - }); - }); - this.hoverSelectRectangle = false; - this.isSelecting = false; - this.startSelectPoints = { - x: 0, - y: 0 - }; - } - initSelect() { - this.startTextPoints = { - x: webDraft.mPosition.x, - y: webDraft.mPosition.y - }; - console.log(this.startTextPoints) - } - startSelect() { - if ($("#textRectangle").empty()) { - let x, y, width, height; - if (this.startTextPoints.x <= webDraft.mPosition.x) { - x = this.startTextPoints.x; - width = webDraft.mPosition.x - this.startTextPoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startTextPoints.x - webDraft.mPosition.x; - } - if (this.startTextPoints.y <= webDraft.mPosition.y) { - y = this.startTextPoints.y; - height = webDraft.mPosition.y - this.startTextPoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startTextPoints.y - webDraft.mPosition.y; - } - $("#textRectangle") - .empty() - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "min-height": height + "px", - "width": width + "px", - "height": height + "px", - "border": "1px dashed #fff", - "background": "transparent" - }); - this.isSelecting = true; - } - } - showTextOptions() { - this.isSelecting = false; - $("#textOptions").show(); - } - putLayer() { - $("#textRectangle").css('border', 'none'); - - if ($('#textRectangle').html() === '') { - return false; - } - - let top = parseInt($("#textRectangle").css('top')); - let left = parseInt($("#textRectangle").css('left')); - html2canvas($('#textRectangle'), { - onrendered: function (canvas) { - let widthImg = canvas.width; - let heightImg = canvas.height; - layers.newLayer(); - layers.setLayerSize(layers.activeId, widthImg, heightImg); - layers.setLayerPosition(layers.activeId, top, left); - webDraft.positionElements(); - - ctx.drawImage(canvas, 0, 0); - - layers.saveState(); - $("#textRectangle") - .css({ - "top": "0px", - "left": "0px", - "border": "1px dashed rgb(255, 255, 255)" - }) - .width(0) - .height(0) - .hide() - .empty(); - } - }); - } -} -class WebDraft { - constructor() { - this.isLoaded = false; - this.title = "WebDraft"; - this.click = { - left: false, //left mouse button - right: false //right mouse button - }; - this.mPosition = {//mouse position on draw - x: 0, - y: 0 - }; - this.draw = { - width: 600, - height: 600, - thisParent: "#drawHandler", - selectorId: "#draw", - eventHandler: "#eventHandler", - bg: "url('pic/transparent.png') repeat" - }; - this.shadow = { - isShadow: false, - blur: 1, - offsetX: 0, - offsetY: 0, - color: "#232324" - }; - this.sensitivityPoints = 1000; - this.size = 10; - this.color = "#000000"; - this.selectedTool = PENCIL; - } - resize() {} - makeid() { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < 15; i++) - text += possible.charAt(Math.floor(Math.random() * possible.length)); - - return text; - } - positionElements() { - var image = { - id: new Array(), - img: new Array() - }; - - var active = layers.activeId; - for (var i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - image.img[i] = ctx.getImageData(0, 0, this.draw.width, this.draw.height); - image.id[i] = layers.list[i].id; - } - } - - $(this.draw.selectorId + "," + this.draw.eventHandler).css({ - "width": this.draw.width, - "height": this.draw.height - }); - $(this.draw.thisParent).css({ - "background": this.draw.bg, - "width": this.draw.width, - "height": this.draw.height - }); - - if (this.draw.width >= $("#content").width()) { - $(this.draw.thisParent).css({"margin-left": "0px"}); - } else { - $(this.draw.thisParent).css({"margin-left": ($("#content").width() - this.draw.width) / 2}); - } - - if (this.draw.height >= $("#content").height()) { - $(this.draw.thisParent).css({"margin-top": "0px"}); - } else { - $(this.draw.thisParent).css({"margin-top": ($("#content").height() - this.draw.height) / 2}); - } - - for (var i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - ctx.putImageData(image.img[i], 0, 0); - layers.saveState(); - } - } - - if (active !== "") - layers.select(active); - - $("title").text(this.title + ' [' + this.draw.width + " x " + this.draw.height + ']'); - $("html, body, #paint").css({"visibility": "visible"}); - } - moveEraseRect(event) { - $("#eraseRect").css({ - "width": this.size, - "height": this.size, - "top": event.pageY - (this.size / 2) + "px", - "left": event.pageX - (this.size / 2) + "px" - }); - } - erase(event) { - this.moveEraseRect(event); - ctx.clearRect(this.mPosition.x - this.size / 2, this.mPosition.y - this.size / 2, this.size, this.size); - - points[layers.activeId] = []; -// do poprawy - } - clear() { - $(this.draw.selectorId).empty(); - $("#listLayers").empty(); - - points = {}; - - this.init(); - } - colorsampler(event) { - var x = this.mPosition.x; - var y = this.mPosition.y; - var p = ctx.getImageData(x, y, 1, 1).data; - var colorCode; - var r = p[0]; - var g = p[1]; - var b = p[2]; - var alpha = p[3]; - var a = Math.floor((100 * alpha) / 255) / 100; - - if (a <= 0) { - colorCode = ((r << 16) | (g << 8) | b).toString(16); - } else { - colorCode = "rgba(" + r + "," + g + "," + b + "," + a + ")"; - } - - if (colorCode === "0") - colorCode = "transparent"; - - $("#textColorSampler").text(colorCode); - $("#colorBoxSampler").css({"background-color": colorCode}); - } - colorsamplerSetcolor() { - if ($("#textColorSampler").text() !== 'null') { - $("#generalColor .color").css({"background": $("#textColorSampler").text()}); - $("#firstColor").val($("#textColorSampler").text()); - this.color = $("#textColorSampler").text(); - } - } - mousePosition(event) { - var touch = undefined; - - if (event.originalEvent.touches) { - touch = event.originalEvent.touches[0]; - - var pos_x = event.pageX || touch.pageX; - var pos_y = event.pageY || touch.pageY; - - - } else { - var pos_x = event.pageX; - var pos_y = event.pageY; - } - - this.mPosition.x = pos_x - parseInt($(this.draw.selectorId).offset().left) - parseInt($(canvas).css('left')); - this.mPosition.y = pos_y - parseInt($(this.draw.selectorId).offset().top) - parseInt($(canvas).css('top')); - - $("#mousePosition").text(this.mPosition.x + " , " + this.mPosition.y); - if (this.mPosition.x < 0 || this.mPosition.x > $(this.draw.selectorId).width() - || this.mPosition.y < 0 || this.mPosition.y > $(this.draw.selectorId).height()) { - $("#mousePosition").empty(); - } - } - _mousedown(event) { - this.mousePosition(event); - if ("buttons" in event) { - if (event.buttons == 1) { - this.click.left = true; - } - } - var button = event.which || event.button; - if (button == 1) { - this.click.left = true; - } - if (!this.click.right && this.click.left) { - if (this.selectedTool !== SELECT) - points[layers.activeId].push({x: this.mPosition.x, y: this.mPosition.y}); - - switch (this.selectedTool) { - case PENCIL : - case ERASER : - draw.drawing(); - break; - case SELECT : - select.initSelect(); - break; - case RECTANGLE : - case CIRCLE : - shapes.startShape(); - break; - case TEXT : - text.initSelect(); - break; - case COLORSAMPLER : - this.colorsamplerSetcolor(); - $("#pencil").click(); - break; - } - } - } - _mouseup(event) { - this.click.left = false; - this.click.right = false; - - ctx.beginPath(); - ctx.stroke(); - - switch (this.selectedTool) { - case SELECT: - select.selectOpt(); - break; - case TEXT : - text.showTextOptions(); - break; - case RECTANGLE : - shapes.drawRect(); - break; - case CIRCLE : - shapes.drawCircle(); - break; - } - - layers.saveState(); - } - _mousemove(event) { - this.mousePosition(event); - - switch (this.selectedTool) { - case ERASER: - this.moveEraseRect(event); - break; - case COLORSAMPLER : - this.colorsampler(); - break; - case SELECT : - if ( - !select.isSelecting && this.mPosition.x <= parseInt($("#selectRectangle").css("left")) + $("#selectRectangle").width() - && this.mPosition.x >= parseInt($("#selectRectangle").css("left")) - && this.mPosition.y <= parseInt($("#selectRectangle").css("top")) + $("#selectRectangle").height() - && this.mPosition.y >= parseInt($("#selectRectangle").css("top")) - ) { - select.hoverSelectRectangle = true; - $("#selectRectangle").css({"z-index": 5}); - } else { - select.hoverSelectRectangle = false; - $("#selectRectangle").css({"z-index": 3}); - } - break; - case TEXT : - if ( - !text.isSelecting && this.mPosition.x <= parseInt($("#textRectangle").css("left")) + $("#textRectangle").width() - && this.mPosition.x >= parseInt($("#textRectangle").css("left")) - && this.mPosition.y <= parseInt($("#textRectangle").css("top")) + $("#textRectangle").height() - && this.mPosition.y >= parseInt($("#textRectangle").css("top")) - ) { - text.hoverSelectRectangle = true; - $("#textRectangle").css({"z-index": 5}); - } else { - text.hoverSelectRectangle = false; - $("#textRectangle").css({"z-index": 3}); - } - break; - } - - if (this.click.left && !this.click.right) { - if (this.selectedTool !== SELECT) - points[layers.activeId].push({x: this.mPosition.x, y: this.mPosition.y}); - - switch (this.selectedTool) { - case PENCIL : - case ERASER : - ctx.lineTo(this.mPosition.x, this.mPosition.y); - ctx.stroke(); - break; - case WEB : - draw.drawWeb(); - break; - case SELECT : - if (!select.hoverSelectRectangle) - select.startSelect(); - break; - case TEXT : - if (!text.hoverSelectRectangle) - text.startSelect(); - break; - case RECTANGLE : - shapes.prepareRect(); - break; - case CIRCLE : - shapes.prepareCircle(); - break; - } - } - } - loadParts() { - $.get('parts/buttons.part.html', function (data) { - $('[data-id=buttons]').html(data); - events.buttons(); - }); - $.get('parts/info.part.html', function (data) { - $('#info').html(data); - events.info(); - }); - $.get('parts/color-picker.part.html', function (data) { - $('[data-id=color-picker]').html(data); - events.color(); - }); - $.get('parts/sliders.part.html', function (data) { - $('[data-id=sliders]').html(data); - events.sliders(); - }); - this.isLoaded = true; - } - init() { - let $this = this; - if (!this.isLoaded) { - this.loadParts(); - } else { - layers.newLayer(); - } - //events on #draw - $(this.draw.eventHandler) - .hover(function () { - if (typeof ctx !== 'undefined') { - ctx.beginPath(); - ctx.stroke(); - } - if (this.selectedTool === ERASER) { - $("#eraseRect").show(); - } - - }) - .bind("contextmenu", function (event) { - if (!DEBUG) { - event.preventDefault(); - } - this.click.right = true; - this.click.left = false; - }) - .on('mousedown', function (e) { - $this._mousedown(e); - }) - .on('mouseup', function (e) { - $this._mouseup(e); - }) - .on('mousemove', function (e) { - $this._mousemove(e); - }) - .mouseleave(function () { - ctx.stroke(); - layers.saveState(); - if (this.selectedTool === ERASER) - $("#eraseRect").hide(); - }) - .dblclick(function () { - switch (this.selectedTool) { - case SELECT: - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px" - }) - .width(0) - .height(0) - .hide(); - break; - case TEXT : - text.putLayer(); - - break; - } - }); - } -} - -const DEBUG = 0; - -const PENCIL = 'pencil'; -const SELECT = 'select'; -const ERASER = 'eraser'; -const WEB = 'web'; -const TEXT = 'text'; -const RECTANGLE = 'rectangle'; -const CIRCLE = 'circle'; -const COLORSAMPLER = 'colorsampler'; - - -let items = [ - { - text: 'Clear', - shortcut: 'ctrl+del', - icon: 'fas fa-ban', - onclick: function () { - webDraft.clear(); - } - }, - { - text: 'Upload image', - shortcut: 'ctrl+o', - icon: 'fas fa-upload', - onclick: function () { - $('#fileUploader').click(); - } - }, - { - text: 'Save image', - shortcut: 'ctrl+s', - icon: 'fas fa-download', - onclick: function () { - file.download(); - } - }, - { - text: 'Color', - icon: 'fas fa-tint', - submenu: [ - { - text: 'Invert', - icon: 'fas fa-adjust', - onclick: function () { - layers.negative(); - } - }, - { - text: 'Colorpicker', - icon: 'fas fa-eye-dropper', - onclick: function () { - $('#colorsampler').click(); - } - } - ] - } -]; - -let context_menu = new Contextmenu(items); -let camera = new Camera(); -let file = new File(); -let shapes = new Shapes(); -let draw = new Draw(); -let text = new Text(); -let resizer = new Resizer(); -let layers = new Layers(); -let select = new Select(); -let webDraft = new WebDraft(); - - -let canvas, - ctx, - randomId, - points = {}; - - -$(window) - .resize(function () { - webDraft.resize(); - webDraft.positionElements(); - }); -$(document) - .ready(function (event) { - if (/mobile/i.test(navigator.userAgent)) { - webDraft.draw.width = window.innerWidth - 10; - webDraft.draw.height = window.innerHeight - 30; - } - - webDraft.init(); - $("#selectRectangle, #textRectangle") - .draggable({snap: false}) - .css({"position": "absolute"}); - - $("#shadowDot").draggable({ - containment: "#shadowSquare", - scroll: false, - drag: function () { - let shadowY = parseInt($(this).css('top')) - (parseInt($(this).parent().height()) / 2); - let shadowX = parseInt($(this).css('left')) - (parseInt($(this).parent().width()) / 2); - webDraft.shadow.offsetX = shadowX; - webDraft.shadow.offsetY = shadowY; - } - }); - $('#shadowSquare').on('mousedown', function (e) { - let x = e.pageX - $(this).offset().left; - let y = e.pageY - $(this).offset().top; - - $("#shadowDot").css({ - top: (y - 5) + 'px', - left: (x - 5) + 'px' - }); - - let shadowY = parseInt($("#shadowDot").css('top')) - (parseInt($("#shadowDot").parent().height()) / 2); - let shadowX = parseInt($("#shadowDot").css('left')) - (parseInt($("#shadowDot").parent().width()) / 2); - webDraft.shadow.offsetX = shadowX; - webDraft.shadow.offsetY = shadowY; - }); - - - }) - .bind("contextmenu", function (e) { - if (!DEBUG) { - e.preventDefault(); - } - }) - .on('mouseup touchend', function (e) { - webDraft._mouseup(e); - }) - .on('mousemove touchmove', function (e) { - webDraft._mousemove(e); - }); - -function hexToRgba(hex, opacity) { - hex = hex.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - const a = opacity / 100; - return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; -} \ No newline at end of file diff --git a/index.html b/index.html index 7968044..3321ef7 100644 --- a/index.html +++ b/index.html @@ -1,80 +1,12 @@ - + - - WebDraft - - - - - - - - - - - - -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    Del
    -
    Delete
    -
    Ctrl + X
    -
    Cut
    -
    Ctrl + C
    -
    Copy
    -
    Ctrl + V
    -
    Paste
    -
    -
    -
    - -
    -
    -
    -   |   - Created by rtCoder -
    -
    - - - - - - - - - - - + + + + WebDraft v3 + + +
    + + diff --git a/js/modules/Camera.js b/js/modules/Camera.js deleted file mode 100644 index d842c66..0000000 --- a/js/modules/Camera.js +++ /dev/null @@ -1,171 +0,0 @@ -class Camera { - constructor() { - this.cameraStream = null; - this.filters = { - 'sepia': false, - 'noise': false, - 'greyscale': false, - 'negative': false - }; - let $this = this; - this.filterInterval = null; - $.get('parts/camera.part.html', function (data) { - $('body').append('
    '); - - $('#camera').html(data); - - $('#snap').click($this.snap); - $('#saveSnapOnComputer').click($this.saveOnComputer); - $('#applySnap').click($this.applySnap); - $('#cancelSnap').click($this.cancelSnap); - $('#switchFullscreen').click(function (e) { - $('#camera').toggleClass('fullscreen'); - }) - $('#closeCamera').click(function () { - $this.stop(); - }); - $('.filter-checkbox').change(function (e) { - let filterName = $(this).parent().find('.filter').data('id'); - $this.filters[filterName] = $(this).is(':checked'); - }); - }); - } - - init() { - let $this = this; - $('#camera').addClass('opened'); - let video = document.getElementById('video'); - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) { - video.src = window.URL.createObjectURL(stream); - video.play(); - $this.cameraStream = stream; - $('#camera #cameraTitle').text($this.cameraStream.getTracks()[0].label); - $this.setFilter(); - }); - } - } - - stop() { - let $this = this; - let track = $this.cameraStream.getTracks()[0]; - track.stop(); - $this.cameraStream = null; - $('#camera').removeClass('opened'); - } - - snap() { - let snapImage = document.getElementById('snapImage'); - let snapImageContext = snapImage.getContext('2d'); - let video = document.getElementById('video'); - let additionalLayer = document.getElementById('additional'); - let additionalLayerContext = additionalLayer.getContext('2d'); - let videoImageData = additionalLayerContext.getImageData(0, 0, additionalLayer.width, additionalLayer.height); - snapImageContext.putImageData(videoImageData, 0, 0); - $('#camera').addClass('snapped'); - } - - saveOnComputer() { - file.downloadFromCamera('snapImage'); - } - - cancelSnap() { - let snapImage = document.getElementById('snapImage'); - let snapImageContext = snapImage.getContext('2d'); - snapImageContext.clearRect(0, 0, snapImage.width, snapImage.height); - $('#camera').removeClass('snapped'); - if (this.filterInterval) { - clearInterval(this.filterInterval); - } - } - - applySnap() { - let snapImage = document.getElementById('snapImage'); - layers.newLayer(); - layers.setLayerSize(layers.activeId, snapImage.width, snapImage.height); - webDraft.positionElements(); - ctx.drawImage(snapImage, 0, 0); - layers.saveState(); - $('#camera').removeClass('snapped'); - } - - setFilter() { - let additionalLayer = document.getElementById('additional'); - let additionalLayerContext = additionalLayer.getContext('2d'); - let frame = document.getElementById('frame'); - let frameContext = frame.getContext('2d'); - let video = document.getElementById('video'); - let videoImageData = additionalLayerContext.getImageData(0, 0, additionalLayer.width, additionalLayer.height); - if (this.filterInterval) { - clearInterval(this.filterInterval); - } - let $this = this; - setInterval(function (e) { - frameContext.drawImage(video, 0, 0, video.width, video.height); - videoImageData = frameContext.getImageData(0, 0, video.width, video.height); - - for (let i in $this.filters) { - if ($this.filters[i]) { - videoImageData = $this[i + 'Filter'](videoImageData); - } - } - - additionalLayerContext.putImageData(videoImageData, 0, 0); - }, 1) - } - - negativeFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - pixels[i] = 255 - pixels[i]; // red - pixels[i + 1] = 255 - pixels[i + 1]; // green - pixels[i + 2] = 255 - pixels[i + 2]; // blue - } - - return imageData; - } - - greyscaleFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let r = pixels[i]; - let g = pixels[i + 1]; - let b = pixels[i + 2]; - let avg = 0.3 * r + 0.59 * g + 0.11 * b; - - pixels[i] = avg; // red - pixels[i + 1] = avg; // green - pixels[i + 2] = avg; // blue - } - - return imageData; - } - - noiseFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let rand = (0.5 - Math.random()) * 100; - pixels[i] += rand; // red - pixels[i + 1] += rand; // green - pixels[i + 2] += rand; // blue - } - - return imageData; - } - - sepiaFilter(imageData) { - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - let r = pixels[i]; - let g = pixels[i + 1]; - let b = pixels[i + 2]; - let avg = 0.3 * r + 0.59 * g + 0.11 * b; - - pixels[i] = avg + 100; // red - pixels[i + 1] = avg + 50; // green - pixels[i + 2] = avg; // blue - } - - return imageData; - } -} \ No newline at end of file diff --git a/js/modules/Clipboard.js b/js/modules/Clipboard.js deleted file mode 100644 index e69de29..0000000 diff --git a/js/modules/Contextmenu.js b/js/modules/Contextmenu.js deleted file mode 100644 index 4cdc1e2..0000000 --- a/js/modules/Contextmenu.js +++ /dev/null @@ -1,89 +0,0 @@ -class Contextmenu { - constructor(items) { - this.items = items; - let $this = this; - $('body').append(''); - $('#contextmenu').html('
      '); - for (let i in items) { - let li = this.buildItem(this.items[i]); - - $('#contextmenu ul').append(li); - - if (this.items[i].onclick) { - $('#contextmenu ul li').last().click(function (e) { - $this.items[i].onclick(); - $this.hide(e); - }); - } - - if (this.items[i].submenu && this.items[i].submenu.length > 0) { - $('#contextmenu ul li').last().prepend(''); - for (let j in this.items[i].submenu) { - - let li = this.buildItem(this.items[i].submenu[j]); - - $('#contextmenu ul li ul.submenu').append(li); - - if (this.items[i].submenu[j].onclick) { - $('#contextmenu ul li').last().click(function (e) { - $this.items[i].submenu[j].onclick(); - $this.hide(e); - }); - } - } - } - } - - $('body').contextmenu(function (e) { - e.preventDefault(); - $this.show(e); - }).click(function (e) { - if (!$('#contextmenu').is(":hover")) { - $this.hide(e); - } - }); - } - show(e) { - let t = e.pageY; - let l = e.pageX; - $('#contextmenu').removeClass('left-submenu'); - if (t + $('#contextmenu').height() > $(window).height()) { - t = $(window).height() - $('#contextmenu').height(); - } - if (l + $('#contextmenu').width() > $(window).width()) { - l = $(window).width() - $('#contextmenu').width(); - } - if (l + ($('#contextmenu').width() * 2) > $(window).width()) { - $('#contextmenu').addClass('left-submenu'); - } - $('#contextmenu').css({ - top: t + "px", - left: l + "px" - }).show(); - } - hide(e) { - $('#contextmenu').hide(); - } - buildItem(item) { - let arrowLeft = ''; - let arrowRight = ''; - let shortcut = ''; - let text = ''; - let icon = '' + arrowLeft + icon + text + shortcut + arrowRight + ''; - } -} \ No newline at end of file diff --git a/js/modules/Draw.js b/js/modules/Draw.js deleted file mode 100644 index 1a853b7..0000000 --- a/js/modules/Draw.js +++ /dev/null @@ -1,64 +0,0 @@ -class Draw { - drawStyle() { - ctx.lineWidth = webDraft.size; - - if (webDraft.selectedTool === RECTANGLE) { - ctx.lineJoin = ctx.lineCap = 'miter'; - } else { - ctx.lineJoin = ctx.lineCap = 'round'; - } - - if (shapes.fill.isSet === true) { - ctx.fillStyle = hexToRgba(shapes.fill.color, shapes.fill.opacity); - } else { - ctx.fillStyle = "transparent"; - } - - if (webDraft.selectedTool === ERASER) { - ctx.globalCompositeOperation = "destination-out"; - } else { - ctx.globalCompositeOperation = "source-over"; - } - - if (webDraft.shadow.isShadow === true && webDraft.selectedTool !== ERASER) { - ctx.shadowBlur = webDraft.shadow.blur; - ctx.shadowColor = webDraft.shadow.color; - ctx.shadowOffsetX = webDraft.shadow.offsetX; - ctx.shadowOffsetY = webDraft.shadow.offsetY; - } else { - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } - - ctx.strokeStyle = webDraft.color;//line color - } - drawing() { - ctx.beginPath(); - ctx.moveTo(webDraft.mPosition.x, webDraft.mPosition.y); - this.drawStyle(); - ctx.lineTo(webDraft.mPosition.x, webDraft.mPosition.y); - ctx.stroke(); - } - drawWeb() { - ctx.beginPath(); - this.drawStyle(); - ctx.moveTo(points[layers.activeId][points[layers.activeId].length - 2].x, points[layers.activeId][points[layers.activeId].length - 2].y); - ctx.lineTo(points[layers.activeId][points[layers.activeId].length - 1].x, points[layers.activeId][points[layers.activeId].length - 1].y); - ctx.stroke(); - let dx, dy, d; - let len = points[layers.activeId].length; - for (let i = 0; i < len; i++) { - dx = points[layers.activeId][i].x - points[layers.activeId][points[layers.activeId].length - 1].x; - dy = points[layers.activeId][i].y - points[layers.activeId][points[layers.activeId].length - 1].y; - d = dx * dx + dy * dy; - if (d < webDraft.sensitivityPoints) { - ctx.beginPath(); - this.drawStyle(); - ctx.moveTo(points[layers.activeId][points[layers.activeId].length - 1].x + (dx * 0.2), points[layers.activeId][points[layers.activeId].length - 1].y + (dy * 0.2)); - ctx.lineTo(points[layers.activeId][i].x - (dx * 0.2), points[layers.activeId][i].y - (dy * 0.2)); - ctx.stroke(); - } - } - } -} \ No newline at end of file diff --git a/js/modules/File.js b/js/modules/File.js deleted file mode 100644 index 0fbcd65..0000000 --- a/js/modules/File.js +++ /dev/null @@ -1,67 +0,0 @@ -class File { - constructor() { - $('body').append(''); - } - - upload(event) { - let input = event.target; - let widthImg; - let heightImg; - let output = new Image(); - - output.src = URL.createObjectURL(input.files[0]); - - output.onload = function () { - widthImg = this.width; - heightImg = this.height; - - layers.newLayer(); - layers.setLayerSize(layers.activeId, widthImg, heightImg); - webDraft.positionElements(); - - ctx.drawImage(output, 0, 0); - - layers.saveState(); - keys.O = false; - }; - } - download() { - $(webDraft.draw.selectorId).append(''); - - let temp_c = document.getElementById("tmpCanvas"); - let temp_ctx = temp_c.getContext("2d"); - $("#tmpCanvas").width(webDraft.draw.width).height(webDraft.draw.height); - - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string" && layers.list[i].visible === true) { - let imgData = document.getElementById(layers.list[i].id); - let top = parseInt($("#" + layers.list[i].id).css("top")); - let left = parseInt($("#" + layers.list[i].id).css("left")); - temp_ctx.drawImage(imgData, top, left); - } - } - - temp_c.toBlob(function (blob) { - saveAs(blob, "WebDraft-image.png"); - }); - - $("#tmpCanvas").remove(); - } - downloadFromCamera(id = null) { - let imgData = document.getElementById(id); - $(webDraft.draw.selectorId).append(''); - - let temp_c = document.getElementById("tmpCanvas"); - let temp_ctx = temp_c.getContext("2d"); - $("#tmpCanvas").width(imgData.width).height(imgData.height); - - temp_ctx.drawImage(imgData, 0, 0); - - temp_c.toBlob(function (blob) { - saveAs(blob, "WebDraft-camera-photo.png"); - }); - - $("#tmpCanvas").remove(); - - } -} \ No newline at end of file diff --git a/js/modules/Layers.js b/js/modules/Layers.js deleted file mode 100644 index 9d99bd3..0000000 --- a/js/modules/Layers.js +++ /dev/null @@ -1,349 +0,0 @@ -class Layers { - constructor() { - $.get('parts/layers.part.html', function (data) { - $('#layers').html(data); - layers.newLayer(); - }); - - this.activeId = ""; - this.list = []; - } - setLayerPosition(layerId, top, left) { - $("canvas#" + layerId).css({ - "top": top, - "left": left - }); - } - setLayerSize(layerId, width, height) { - let image = { - id: new Array(), - img: new Array() - }; - - let active = layers.activeId; - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - image.img[i] = ctx.getImageData(0, 0, webDraft.draw.width, webDraft.draw.height); - image.id[i] = layers.list[i].id; - } - } - - if (layerId === "") { - webDraft.draw.width = width; - webDraft.draw.height = height; - $("canvas.canvas-draw").attr({ - "width": webDraft.draw.width, - "height": webDraft.draw.height - }); - } else { - $("canvas#" + layerId).attr({ - "width": width, - "height": height - }); - webDraft.draw.width = 0; - webDraft.draw.height = 0; - $("canvas.canvas-draw").each(function () { - let w = parseInt($(this).attr("width")); - let h = parseInt($(this).attr("height")); - if (webDraft.draw.width < w) - webDraft.draw.width = w; - - if (webDraft.draw.height < h) - webDraft.draw.height = h; - }); - } - - for (let i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - ctx.putImageData(image.img[i], 0, 0); - layers.saveState(); - } - } - - if (active !== "") - layers.select(active); - } - saveState() { - let imgSrc = document.getElementById(layers.activeId).toDataURL(); - $(".layerView[data-id=" + layers.activeId + "]").find("img").attr("src", imgSrc).show(); - } - newLayer() { - let j; - if (isNaN(parseInt($(".layerView:last").attr("id")))) { - j = 0; - } else { - j = parseInt($(".layerView:last").attr("id")) + 1; - } - - let countViews = $("#listLayers").children(".layerView").length; - - if (countViews < 15) { - randomId = webDraft.makeid(); - - $(webDraft.draw.selectorId).append(''); - $("#listLayers").append(` -
      -
      - -
      -
      - -
      ` - ); - - let new_layer = { - visible: true, - id: randomId, - top: 0, - left: 0 - }; - layers.list.push(new_layer); - - $(".layerView").click(function () { - if (!$(this).hasClass("hidden")) { - $(".layerView").removeClass("active"); - $(this).addClass("active"); - - let identifier = $(this).attr("data-id"); - - layers.select(identifier); - } - }); - $(".hideLayer").click(function () { - let nr = $(this).parent(".layerView").attr("id"); - - $(this).hide(); - $(this) - .parent(".layerView") - .find(".showLayer") - .css({"display": "block"}); - - layers.hide(nr); - }); - $(".showLayer").click(function () { - let nr = $(this).parent(".layerView").attr("id"); - - $(this).hide(); - $(this) - .parent(".layerView") - .find(".hideLayer") - .css({"display": "block"}); - - layers.show(nr); - }); - $(".layerView[data-id=" + randomId + "]").click(); - points[randomId] = []; - - } - } - delete() { - let identifier = $(".layerView.active").attr("data-id"); - let nr = $(".layerView.active").attr("id"); - - let countViews = $(".layerView").length; - - if (countViews > 1) { - let i = parseInt(nr); - $(".layerView#" + i).remove(); - $("canvas#" + identifier).remove(); - for (i in layers.list) { - if (!$('canvas#' + layers.list[i].id).length) { - layers.list.splice(i, 1); - } - } - - let j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - - webDraft.draw.width = 0; - webDraft.draw.height = 0; - $("canvas.canvas-draw").each(function () { - const w = parseInt($(this).attr("width")); - const h = parseInt($(this).attr("height")); - if (webDraft.draw.width < w) - webDraft.draw.width = w; - - if (webDraft.draw.height < h) - webDraft.draw.height = h; - }); - $(".layerView").first().click(); - webDraft.positionElements(); - } - } - hide(nr) { - let i = parseInt(nr); - - $(".layerView#" + i).addClass("hidden"); - $("canvas#" + layers.list[i].id).addClass("invisible"); - - let j = 0; - $("canvas.canvas-draw").not('#snapImage').each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - // $(".layerView#" + i).next().not(".hidden").click() - } - show(nr) { - let i = parseInt(nr); - - $(".layerView#" + i).removeClass("hidden"); - $("canvas#" + layers.list[i].id).removeClass("invisible"); - - let j = 0; - $("canvas.canvas-draw").not('#snapImage').each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - j = 0; - $(".layerView").each(function () { - $(this).attr({ - "id": j, - "data-id": layers.list[j].id} - ); - j++; - }); - } - select(id) { - if (!$(".layerView[data-id=" + id + "]").hasClass("hidden")) { - canvas = document.getElementById(id); - ctx = canvas.getContext('2d'); - - layers.activeId = id; - } - } - rotate(angle) { - let image = new Image(); - image.src = canvas.toDataURL(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(angle * Math.PI / 180); - ctx.drawImage(image, -image.width / 2, -image.width / 2); - ctx.restore(); - layers.saveState(); - } - mirror(direction) { - if (direction === 'horizontal' || direction === 'vertical') { - let image = new Image(); - image.src = canvas.toDataURL(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.translate(canvas.width / 2, canvas.height / 2); - if (direction === 'vertical') - ctx.scale(-1, 1); - else if (direction === 'horizontal') - ctx.scale(1, -1); - ctx.drawImage(image, -image.width / 2, -image.height / 2); - ctx.restore(); - layers.saveState(); - } - } - negative() { - let destX = 0; - let destY = 0; - - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - let pixels = imageData.data; - for (let i = 0; i < pixels.length; i += 4) { - pixels[i] = 255 - pixels[i]; // red - pixels[i + 1] = 255 - pixels[i + 1]; // green - pixels[i + 2] = 255 - pixels[i + 2]; // blue - // i+3 is alpha (the fourth element) - } - - // overwrite original image - ctx.putImageData(imageData, 0, 0);//add the function call in the imageObj.onload - layers.saveState(); - } - moveUp() { - let firstID = $(".layerView").first().attr("data-id"); - if (firstID !== layers.activeId) { - let thisNr = $(".layerView[data-id=" + layers.activeId + "]").attr("id"); - let prevNr = parseInt(thisNr) - 1; - let prevId = $(".layerView#" + prevNr).attr("data-id"); - let thisLayer = $(".layerView#" + thisNr); - let prevLayer = $(".layerView#" + prevNr); - - thisLayer.insertBefore(".layerView#" + prevNr); - thisLayer.removeAttr("id").attr("id", prevNr); - prevLayer.removeAttr("id").attr("id", thisNr); - - $("canvas#" + layers.activeId).insertBefore("canvas#" + prevId); - - let j = 0; - $("canvas.canvas-draw").each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - return true; - } - return false; - } - moveDown() { - let lastID = $(".layerView").last().attr("data-id"); - if (lastID !== layers.activeId) { - let thisNr = $(".layerView[data-id=" + layers.activeId + "]").attr("id"); - let nextNr = parseInt(thisNr) + 1; - let nextId = $(".layerView#" + nextNr).attr("data-id"); - let thisLayer = $(".layerView#" + thisNr); - let prevLayer = $(".layerView#" + nextNr); - - thisLayer.insertAfter(".layerView#" + nextNr); - thisLayer.removeAttr("id").attr("id", nextNr); - prevLayer.removeAttr("id").attr("id", thisNr); - - $("canvas#" + layers.activeId).insertAfter("canvas#" + nextId); - let j = 0; - $("canvas.canvas-draw").each(function () { - layers.list[j].id = $(this).attr("id"); - - if ($(this).hasClass("invisible")) - layers.list[j].visible = false; - else - layers.list[j].visible = true; - - j++; - }); - - return true; - } - return false; - } -} \ No newline at end of file diff --git a/js/modules/Resizer.js b/js/modules/Resizer.js deleted file mode 100644 index d2666fa..0000000 --- a/js/modules/Resizer.js +++ /dev/null @@ -1,51 +0,0 @@ -class Resizer { - constructor() { - let $this = this; - $.get('parts/resizer.part.html', function (data) { - $('#resizer').html(data); - - $("#resizer input[type=number]").keyup(function (e) { - $this.onkeyup(e); - }); - }); - } - show() { - $("#resizer").show(); - $("input[type=number]#drawWidth").val(webDraft.draw.width); - $("input[type=number]#drawHeight").val(webDraft.draw.height); - $("#resizeinfo").html(webDraft.draw.width + " " + webDraft.draw.height); - } - cancel() { - $("#resizer").hide(); - $("input[type=number]#drawWidth").val(webDraft.draw.width); - $("input[type=number]#drawHeight").val(webDraft.draw.height); - } - apply() { - $("#resizer").hide(); - let w, h; - if ($("#allLayersResizing").is(":checked")) { - w = $("input[type=number]#drawWidth").val(); - h = $("input[type=number]#drawHeight").val(); - - layers.setLayerSize('', w, h); - } else { - w = $("input[type=number]#drawWidth").val(); - h = $("input[type=number]#drawHeight").val(); - - layers.setLayerSize(layers.activeId, w, h); - } - webDraft.positionElements(); - } - onkeyup(e) { - this.onchange(); - - if (e.keyCode === 13) { - $("#apply").click(); - } - } - onchange() { - let xSize = parseInt($("input[type=number]#drawWidth").val()); - let ySize = parseInt($("input[type=number]#drawHeight").val()); - $("#resizeinfo").html(xSize + " " + ySize); - } -} \ No newline at end of file diff --git a/js/modules/Select.js b/js/modules/Select.js deleted file mode 100644 index f5b35d7..0000000 --- a/js/modules/Select.js +++ /dev/null @@ -1,132 +0,0 @@ -class Select { - constructor() { - $('#drawHandler').append('
      '); - this.hoverSelectRectangle = false; - this.isSelecting = false; - this.startSelectPoints = { - x: 0, - y: 0 - }; - } - initSelect() { - this.startSelectPoints = { - x: webDraft.mPosition.x, - y: webDraft.mPosition.y - }; - } - startSelect() { - let x, y, width, height; - if (this.startSelectPoints.x <= webDraft.mPosition.x) { - x = this.startSelectPoints.x; - width = webDraft.mPosition.x - this.startSelectPoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startSelectPoints.x - webDraft.mPosition.x; - } - if (this.startSelectPoints.y <= webDraft.mPosition.y) { - y = this.startSelectPoints.y; - height = webDraft.mPosition.y - this.startSelectPoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startSelectPoints.y - webDraft.mPosition.y; - } - $("#selectRectangle") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "width": width + "px", - "height": height + "px", - "border": "1px dashed #fff", - "background": "transparent" - }); - this.isSelecting = true; - } - selectOpt() { - this.isSelecting = false; - } - delSelectedPart() { - if ($("#selectRectangle").css("background-image") === "none") {//if selectRectangle is empty then clear part of image hovered by it - xpos = parseInt($("#selectRectangle").css("left")); - ypos = parseInt($("#selectRectangle").css("top")); - ctx.clearRect(xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height()); - } else { //else clear only selectRectangle background - $("#selectRectangle").css({"background": "transparent"}); - } - - layers.saveState(); - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px", - "background": "transparent" - }) - .width(0) - .height(0) - .hide(); - } - copySelectedPart() { - if ($("#selectRectangle").css("background-image") === "none") { - $(webDraft.draw.selectorId).append(''); - - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var testC = document.getElementById("tmpCanvas"); - var testCtx = testC.getContext('2d'); - - testCtx.drawImage(canvas, xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height(), 0, 0, $("#selectRectangle").width(), $("#selectRectangle").height()); - - var bgImg = testC.toDataURL(); - - $("#tmpCanvas").remove(); - $("#selectRectangle").css({"background": "url(" + bgImg + ") -1px -1px no-repeat"}); - } - } - cutSelectedPart() { - $(webDraft.draw.selectorId).append(''); - - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var testC = document.getElementById("tmpCanvas"); - var testCtx = testC.getContext('2d'); - - testCtx.drawImage(canvas, xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height(), 0, 0, $("#selectRectangle").width(), $("#selectRectangle").height()); - - var bgImg = testC.toDataURL(); - - $("#tmpCanvas").remove(); - $("#selectRectangle").css({"background": "url(" + bgImg + ") -1px -1px no-repeat "}); - - ctx.clearRect(xpos, ypos, $("#selectRectangle").width(), $("#selectRectangle").height()); - layers.saveState(); - } - pasteSelectedPart() { - if ($("#selectRectangle").css("background-image") !== "none") { - var xpos = parseInt($("#selectRectangle").css("left")); - var ypos = parseInt($("#selectRectangle").css("top")); - var img = new Image(); - var bg = $("#selectRectangle") - .css("background-image") - .replace('url(', '') - .replace(')', '') - .replace('"', '') - .replace('"', ''); - img.onload = function () { - ctx.drawImage(img, xpos, ypos); //save part of image when loaded - - layers.saveState(); //and then update layer preview - }; - img.src = bg; - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px", - "background": "transparent" - }) - .width(0) - .height(0) - .hide(); - } - } -} -; \ No newline at end of file diff --git a/js/modules/Shapes.js b/js/modules/Shapes.js deleted file mode 100644 index 95552c2..0000000 --- a/js/modules/Shapes.js +++ /dev/null @@ -1,162 +0,0 @@ -class Shapes { - constructor() { - $('#drawHandler').append(` -
      -
      `); - this.startShapePoints = { - x: 0, - y: 0 - }; - this.fill = { - isSet: false, - color: "#ffffff", - opacity: 100 - }; - } - - startShape() { - this.startShapePoints.x = webDraft.mPosition.x; - this.startShapePoints.y = webDraft.mPosition.y; - } - prepareRect() { - let x, y, width, height; - if (this.startShapePoints.x <= webDraft.mPosition.x) { - x = this.startShapePoints.x; - width = webDraft.mPosition.x - this.startShapePoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startShapePoints.x - webDraft.mPosition.x; - } - if (this.startShapePoints.y <= webDraft.mPosition.y) { - y = this.startShapePoints.y; - height = webDraft.mPosition.y - this.startShapePoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startShapePoints.y - webDraft.mPosition.y; - } - $("#prepareRect") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "width": width + "px", - "height": height + "px", - "border": webDraft.size + "px solid " + webDraft.color - }); - if (this.fill.isSet) { - $("#prepareRect").css({"background": hexToRgba(this.fill.color, this.fill.opacity)}); - } else { - $("#prepareRect").css({"background": "transparent"}); - } - if (webDraft.shadow.isShadow) { - $("#prepareRect").css({"box-shadow": webDraft.shadow.offsetX + "px " + webDraft.shadow.offsetY + "px " + webDraft.shadow.blur + "px " + webDraft.shadow.color}); - } else { - $("#prepareRect").css({"box-shadow": "none"}); - } - } - drawRect() { - let x, y, width, height; - if (this.startShapePoints.x >= 0 && this.startShapePoints.y >= 0) { - if (this.startShapePoints.x <= webDraft.mPosition.x) { - x = this.startShapePoints.x; - width = webDraft.mPosition.x - this.startShapePoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startShapePoints.x - webDraft.mPosition.x; - } - if (this.startShapePoints.y <= webDraft.mPosition.y) { - y = this.startShapePoints.y; - height = webDraft.mPosition.y - this.startShapePoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startShapePoints.y - webDraft.mPosition.y; - } - $("#prepareRect").hide(); - ctx.beginPath(); - draw.drawStyle(); - ctx.rect(x, y, width, height); - ctx.fill(); - ctx.stroke(); - } - this.startShapePoints.x = -1; - this.startShapePoints.y = -1; - } - prepareCircle() { - let x, y, width, height, radius; - x = this.startShapePoints.x; - y = this.startShapePoints.y; - - if (x <= webDraft.mPosition.x) { - width = webDraft.mPosition.x - x; - } else { - width = x - webDraft.mPosition.x; - } - - if (y <= webDraft.mPosition.y) { - height = webDraft.mPosition.y - y; - } else { - height = y - webDraft.mPosition.y; - } - - if (width > height) { - radius = width; - } else { - radius = height; - } - $("#prepareCircle") - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) - radius + "px", - "left": x + parseInt($(canvas).css('left')) - radius + "px", - "width": radius * 2 + "px", - "height": radius * 2 + "px", - "border": webDraft.size + "px solid " + webDraft.color, - "border-radius": "100%" - }); - - if (this.fill.isSet) { - $("#prepareCircle").css({"background": hexToRgba(this.fill.color, this.fill.opacity)}); - } else { - $("#prepareCircle").css({"background": "transparent"}); - } - - if (webDraft.shadow.isShadow) { - $("#prepareCircle").css({"box-shadow": webDraft.shadow.offsetX + "px " + webDraft.shadow.offsetY + "px " + webDraft.shadow.blur + "px " + webDraft.shadow.color}); - } else { - $("#prepareCircle").css({"box-shadow": "none"}); - } - } - drawCircle() { - let x, y, width, height, radius; - x = this.startShapePoints.x; - y = this.startShapePoints.y; - - if (x >= 0 && y >= 0) { - - if (x <= webDraft.mPosition.x) { - width = webDraft.mPosition.x - x; - } else { - width = x - webDraft.mPosition.x; - } - if (y <= webDraft.mPosition.y) { - height = webDraft.mPosition.y - y; - } else { - height = y - webDraft.mPosition.y; - } - if (width > height) { - radius = width; - } else { - radius = height; - } - $("#prepareCircle").hide(); - - ctx.beginPath(); - draw.drawStyle(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.fill(); - ctx.stroke(); - } - this.startShapePoints.x = -1; - this.startShapePoints.y = -1; - } -} \ No newline at end of file diff --git a/js/modules/Text.js b/js/modules/Text.js deleted file mode 100644 index d0e06f5..0000000 --- a/js/modules/Text.js +++ /dev/null @@ -1,135 +0,0 @@ -class Text { - constructor() { - $('#drawHandler').append('
      '); - - $.get('parts/text-options.part.html', function (data) { - $('#textOptions').html(data); - - - $("#selectFontType").change(function () { - $("#textRectangle").css("font-family", $(this).val()); - }); - $("#selectFontSize").change(function () { - $("#textRectangle").css("font-size", $(this).val() + "px"); - }); - $(".textPostionTool").click(function () { - $(".textPostionTool").removeClass("active"); - $(this).addClass("active"); - $("#textRectangle").css("text-align", $(this).attr('id')); - }); - $(".styleTool").click(function () { - $(this).toggleClass("active"); - switch ($(this).attr("id")) { - case 'bold': - if ($(this).hasClass('active')) - $("#textRectangle").css("font-weight", 'bold'); - else - $("#textRectangle").css("font-weight", 'normal'); - break; - case 'italic': - if ($(this).hasClass('active')) - $("#textRectangle").css("font-style", 'italic'); - else - $("#textRectangle").css("font-style", 'normal'); - break; - case 'underline': - case 'line-through': - var textDecoration = ""; - $(".textDecoration.active").each(function () { - textDecoration += $(this).attr("id") + " "; - }); - if ($(".textDecoration.active").length > 0) - $("#textRectangle").css("text-decoration", textDecoration); - else - $("#textRectangle").css("text-decoration", 'none'); - break; - default: - - } - }); - }); - this.hoverSelectRectangle = false; - this.isSelecting = false; - this.startSelectPoints = { - x: 0, - y: 0 - }; - } - initSelect() { - this.startTextPoints = { - x: webDraft.mPosition.x, - y: webDraft.mPosition.y - }; - console.log(this.startTextPoints) - } - startSelect() { - if ($("#textRectangle").empty()) { - let x, y, width, height; - if (this.startTextPoints.x <= webDraft.mPosition.x) { - x = this.startTextPoints.x; - width = webDraft.mPosition.x - this.startTextPoints.x; - } else { - x = webDraft.mPosition.x; - width = this.startTextPoints.x - webDraft.mPosition.x; - } - if (this.startTextPoints.y <= webDraft.mPosition.y) { - y = this.startTextPoints.y; - height = webDraft.mPosition.y - this.startTextPoints.y; - } else { - y = webDraft.mPosition.y; - height = this.startTextPoints.y - webDraft.mPosition.y; - } - $("#textRectangle") - .empty() - .show() - .css({ - "top": y + parseInt($(canvas).css('top')) + "px", - "left": x + parseInt($(canvas).css('left')) + "px", - "min-height": height + "px", - "width": width + "px", - "height": height + "px", - "border": "1px dashed #fff", - "background": "transparent" - }); - this.isSelecting = true; - } - } - showTextOptions() { - this.isSelecting = false; - $("#textOptions").show(); - } - putLayer() { - $("#textRectangle").css('border', 'none'); - - if ($('#textRectangle').html() === '') { - return false; - } - - let top = parseInt($("#textRectangle").css('top')); - let left = parseInt($("#textRectangle").css('left')); - html2canvas($('#textRectangle'), { - onrendered: function (canvas) { - let widthImg = canvas.width; - let heightImg = canvas.height; - layers.newLayer(); - layers.setLayerSize(layers.activeId, widthImg, heightImg); - layers.setLayerPosition(layers.activeId, top, left); - webDraft.positionElements(); - - ctx.drawImage(canvas, 0, 0); - - layers.saveState(); - $("#textRectangle") - .css({ - "top": "0px", - "left": "0px", - "border": "1px dashed rgb(255, 255, 255)" - }) - .width(0) - .height(0) - .hide() - .empty(); - } - }); - } -} \ No newline at end of file diff --git a/js/modules/WebDraft.js b/js/modules/WebDraft.js deleted file mode 100644 index 91945f4..0000000 --- a/js/modules/WebDraft.js +++ /dev/null @@ -1,381 +0,0 @@ -class WebDraft { - constructor() { - this.isLoaded = false; - this.title = "WebDraft"; - this.click = { - left: false, //left mouse button - right: false //right mouse button - }; - this.mPosition = {//mouse position on draw - x: 0, - y: 0 - }; - this.draw = { - width: 600, - height: 600, - thisParent: "#drawHandler", - selectorId: "#draw", - eventHandler: "#eventHandler", - bg: "url('pic/transparent.png') repeat" - }; - this.shadow = { - isShadow: false, - blur: 1, - offsetX: 0, - offsetY: 0, - color: "#232324" - }; - this.sensitivityPoints = 1000; - this.size = 10; - this.color = "#000000"; - this.selectedTool = PENCIL; - } - resize() {} - makeid() { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < 15; i++) - text += possible.charAt(Math.floor(Math.random() * possible.length)); - - return text; - } - positionElements() { - var image = { - id: new Array(), - img: new Array() - }; - - var active = layers.activeId; - for (var i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - image.img[i] = ctx.getImageData(0, 0, this.draw.width, this.draw.height); - image.id[i] = layers.list[i].id; - } - } - - $(this.draw.selectorId + "," + this.draw.eventHandler).css({ - "width": this.draw.width, - "height": this.draw.height - }); - $(this.draw.thisParent).css({ - "background": this.draw.bg, - "width": this.draw.width, - "height": this.draw.height - }); - - if (this.draw.width >= $("#content").width()) { - $(this.draw.thisParent).css({"margin-left": "0px"}); - } else { - $(this.draw.thisParent).css({"margin-left": ($("#content").width() - this.draw.width) / 2}); - } - - if (this.draw.height >= $("#content").height()) { - $(this.draw.thisParent).css({"margin-top": "0px"}); - } else { - $(this.draw.thisParent).css({"margin-top": ($("#content").height() - this.draw.height) / 2}); - } - - for (var i = 0; i < layers.list.length; i++) { - if (typeof layers.list[i].id === "string") { - layers.select(layers.list[i].id); - ctx.putImageData(image.img[i], 0, 0); - layers.saveState(); - } - } - - if (active !== "") - layers.select(active); - - $("title").text(this.title + ' [' + this.draw.width + " x " + this.draw.height + ']'); - $("html, body, #paint").css({"visibility": "visible"}); - } - moveEraseRect(event) { - $("#eraseRect").css({ - "width": this.size, - "height": this.size, - "top": event.pageY - (this.size / 2) + "px", - "left": event.pageX - (this.size / 2) + "px" - }); - } - erase(event) { - this.moveEraseRect(event); - ctx.clearRect(this.mPosition.x - this.size / 2, this.mPosition.y - this.size / 2, this.size, this.size); - - points[layers.activeId] = []; -// do poprawy - } - clear() { - $(this.draw.selectorId).empty(); - $("#listLayers").empty(); - - points = {}; - - this.init(); - } - colorsampler(event) { - var x = this.mPosition.x; - var y = this.mPosition.y; - var p = ctx.getImageData(x, y, 1, 1).data; - var colorCode; - var r = p[0]; - var g = p[1]; - var b = p[2]; - var alpha = p[3]; - var a = Math.floor((100 * alpha) / 255) / 100; - - if (a <= 0) { - colorCode = ((r << 16) | (g << 8) | b).toString(16); - } else { - colorCode = "rgba(" + r + "," + g + "," + b + "," + a + ")"; - } - - if (colorCode === "0") - colorCode = "transparent"; - - $("#textColorSampler").text(colorCode); - $("#colorBoxSampler").css({"background-color": colorCode}); - } - colorsamplerSetcolor() { - if ($("#textColorSampler").text() !== 'null') { - $("#generalColor .color").css({"background": $("#textColorSampler").text()}); - $("#firstColor").val($("#textColorSampler").text()); - this.color = $("#textColorSampler").text(); - } - } - mousePosition(event) { - var touch = undefined; - - if (event.originalEvent.touches) { - touch = event.originalEvent.touches[0]; - - var pos_x = event.pageX || touch.pageX; - var pos_y = event.pageY || touch.pageY; - - - } else { - var pos_x = event.pageX; - var pos_y = event.pageY; - } - - this.mPosition.x = pos_x - parseInt($(this.draw.selectorId).offset().left) - parseInt($(canvas).css('left')); - this.mPosition.y = pos_y - parseInt($(this.draw.selectorId).offset().top) - parseInt($(canvas).css('top')); - - $("#mousePosition").text(this.mPosition.x + " , " + this.mPosition.y); - if (this.mPosition.x < 0 || this.mPosition.x > $(this.draw.selectorId).width() - || this.mPosition.y < 0 || this.mPosition.y > $(this.draw.selectorId).height()) { - $("#mousePosition").empty(); - } - } - _mousedown(event) { - this.mousePosition(event); - if ("buttons" in event) { - if (event.buttons == 1) { - this.click.left = true; - } - } - var button = event.which || event.button; - if (button == 1) { - this.click.left = true; - } - if (!this.click.right && this.click.left) { - if (this.selectedTool !== SELECT) - points[layers.activeId].push({x: this.mPosition.x, y: this.mPosition.y}); - - switch (this.selectedTool) { - case PENCIL : - case ERASER : - draw.drawing(); - break; - case SELECT : - select.initSelect(); - break; - case RECTANGLE : - case CIRCLE : - shapes.startShape(); - break; - case TEXT : - text.initSelect(); - break; - case COLORSAMPLER : - this.colorsamplerSetcolor(); - $("#pencil").click(); - break; - } - } - } - _mouseup(event) { - this.click.left = false; - this.click.right = false; - - ctx.beginPath(); - ctx.stroke(); - - switch (this.selectedTool) { - case SELECT: - select.selectOpt(); - break; - case TEXT : - text.showTextOptions(); - break; - case RECTANGLE : - shapes.drawRect(); - break; - case CIRCLE : - shapes.drawCircle(); - break; - } - - layers.saveState(); - } - _mousemove(event) { - this.mousePosition(event); - - switch (this.selectedTool) { - case ERASER: - this.moveEraseRect(event); - break; - case COLORSAMPLER : - this.colorsampler(); - break; - case SELECT : - if ( - !select.isSelecting && this.mPosition.x <= parseInt($("#selectRectangle").css("left")) + $("#selectRectangle").width() - && this.mPosition.x >= parseInt($("#selectRectangle").css("left")) - && this.mPosition.y <= parseInt($("#selectRectangle").css("top")) + $("#selectRectangle").height() - && this.mPosition.y >= parseInt($("#selectRectangle").css("top")) - ) { - select.hoverSelectRectangle = true; - $("#selectRectangle").css({"z-index": 5}); - } else { - select.hoverSelectRectangle = false; - $("#selectRectangle").css({"z-index": 3}); - } - break; - case TEXT : - if ( - !text.isSelecting && this.mPosition.x <= parseInt($("#textRectangle").css("left")) + $("#textRectangle").width() - && this.mPosition.x >= parseInt($("#textRectangle").css("left")) - && this.mPosition.y <= parseInt($("#textRectangle").css("top")) + $("#textRectangle").height() - && this.mPosition.y >= parseInt($("#textRectangle").css("top")) - ) { - text.hoverSelectRectangle = true; - $("#textRectangle").css({"z-index": 5}); - } else { - text.hoverSelectRectangle = false; - $("#textRectangle").css({"z-index": 3}); - } - break; - } - - if (this.click.left && !this.click.right) { - if (this.selectedTool !== SELECT) - points[layers.activeId].push({x: this.mPosition.x, y: this.mPosition.y}); - - switch (this.selectedTool) { - case PENCIL : - case ERASER : - ctx.lineTo(this.mPosition.x, this.mPosition.y); - ctx.stroke(); - break; - case WEB : - draw.drawWeb(); - break; - case SELECT : - if (!select.hoverSelectRectangle) - select.startSelect(); - break; - case TEXT : - if (!text.hoverSelectRectangle) - text.startSelect(); - break; - case RECTANGLE : - shapes.prepareRect(); - break; - case CIRCLE : - shapes.prepareCircle(); - break; - } - } - } - loadParts() { - $.get('parts/buttons.part.html', function (data) { - $('[data-id=buttons]').html(data); - events.buttons(); - }); - $.get('parts/info.part.html', function (data) { - $('#info').html(data); - events.info(); - }); - $.get('parts/color-picker.part.html', function (data) { - $('[data-id=color-picker]').html(data); - events.color(); - }); - $.get('parts/sliders.part.html', function (data) { - $('[data-id=sliders]').html(data); - events.sliders(); - }); - this.isLoaded = true; - } - init() { - let $this = this; - if (!this.isLoaded) { - this.loadParts(); - } else { - layers.newLayer(); - } - //events on #draw - $(this.draw.eventHandler) - .hover(function () { - if (typeof ctx !== 'undefined') { - ctx.beginPath(); - ctx.stroke(); - } - if (this.selectedTool === ERASER) { - $("#eraseRect").show(); - } - - }) - .bind("contextmenu", function (event) { - if (!DEBUG) { - event.preventDefault(); - } - this.click.right = true; - this.click.left = false; - }) - .on('mousedown', function (e) { - $this._mousedown(e); - }) - .on('mouseup', function (e) { - $this._mouseup(e); - }) - .on('mousemove', function (e) { - $this._mousemove(e); - }) - .mouseleave(function () { - ctx.stroke(); - layers.saveState(); - if (this.selectedTool === ERASER) - $("#eraseRect").hide(); - }) - .dblclick(function () { - switch (this.selectedTool) { - case SELECT: - $("#selectRectangle") - .css({ - "top": "0px", - "left": "0px" - }) - .width(0) - .height(0) - .hide(); - break; - case TEXT : - text.putLayer(); - - break; - } - }); - } -} \ No newline at end of file diff --git a/js/modules/events.module.js b/js/modules/events.module.js deleted file mode 100644 index e19081e..0000000 --- a/js/modules/events.module.js +++ /dev/null @@ -1,134 +0,0 @@ -var events = { - color: function () { - $('#generalColor').colorpicker({ - onSelect: function (color) { - $('#generalColor .color').css({ - 'background': color.hex - }); - webDraft.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - $('#shadowColor').colorpicker({ - onSelect: function (color) { - $('#shadowColor .color').css({ - 'background': color.hex - }); - webDraft.shadow.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - $('#fillColor').colorpicker({ - onSelect: function (color) { - $('#fillColor .color').css({ - 'background': color.hex - }); - shapes.fill.color = color.hex; - } - }, './vendor/rtcoder/colorpicker/html/colorpicker.html'); - - }, - info: function () { - $(".close-info").click(function () { - $("#info").hide(); - }); - }, - buttons: function () { - $("input[type=checkbox]#isShadow").change(function () { - webDraft.shadow.isShadow = $(this).is(":checked");//return true if is :checked or false if not - - if (webDraft.shadow.isShadow) { - $("#shadowColor, #shadow_slider, #shadowSquare").show(); - } else { - $("#shadowColor, #shadow_slider, #shadowSquare").hide(); - } - }); - $("input[type=checkbox]#isFillSet").change(function () { - shapes.fill.isSet = $(this).is(":checked");//return true if is :checked or false if not - - if (shapes.fill.isSet) { - $("#fillColor, #fillOpacity_slider").show(); - } else { - $("#fillColor, #fillOpacity_slider").hide(); - } - }); - - $(".paintTool").click(function () { - $(".paintTool").removeClass("active"); - $(this).addClass("active"); - - var thisId = $(this).attr("id"); - - webDraft.selectedTool = thisId; - - if (thisId === COLORSAMPLER) { - $("#previewColorSampler").show(); - } else { - $("#previewColorSampler").hide(); - } - - if (thisId === WEB) { - $("#sensitivityPoints_slider").show(); - } else { - $("#sensitivityPoints_slider").hide(); - } - - if (thisId === ERASER) { - $("#eraseRect").show(); - $("#draw, #drawHandler, #eventHandler").css({"cursor": "none"}); - } else { - $("#eraseRect").hide(); - $("#draw, #drawHandler, #eventHandler").css({"cursor": "default"}); - } - - if (thisId !== RECTANGLE) { - $("#prepareRect").hide(); - } - - if (thisId !== CIRCLE) { - $("#prepareCircle").hide(); - } - - if (thisId !== SELECT) { - $("#selectRectangle").hide(); - $("#hint, .hintGroup#selecting").hide(); - } else { - $("#hint, .hintGroup#selecting").show(); - } - - if (thisId !== TEXT) { - $("#textRectangle, #textOptions").hide(); - } - - if (thisId === RECTANGLE || thisId === CIRCLE) { - $("#fillShapeInput").show(); - } else { - $("#fillShapeInput").hide(); - $("input#isFillSet").attr('checked', false).change(); - } - }); - }, - sliders: function () { - //changing size - $("input[type=range]#pointSize").on('mousemove touchmove', function () { - webDraft.size = $(this).val(); - $("#pointSizeValue").text("size:" + $(this).val() + "px"); - }); - //changing shadow blur - $("input[type=range]#ShadowBlur").on('mousemove touchmove', function () { - webDraft.shadow.blur = $(this).val(); - $("#ShadowBlurValue").text("blur:" + $(this).val() + "px"); - }); - //changing sensitivity of web points - $("input[type=range]#sensitivityPoints").on('mousemove touchmove', function () { - webDraft.sensitivityPoints = $(this).val(); - $("#sensitivityPointsValue").text("sensitivity:" + Math.floor($(this).val() / 100) + "%"); - }); - //changing fill opacity - $("input[type=range]#fillOpacity").on('mousemove touchmove', function () { - shapes.fill.opacity = $(this).val(); - $("#fillOpacityValue").text("fill opacity:" + Math.floor($(this).val()) + "%"); - }); - } - -}; \ No newline at end of file diff --git a/js/modules/keys.module.js b/js/modules/keys.module.js deleted file mode 100644 index 9571840..0000000 --- a/js/modules/keys.module.js +++ /dev/null @@ -1,171 +0,0 @@ -var keys = { - Enter: false, //press Enter - Esc: false, //press Escape (Esc) - f11: false, //press F11 - f12: false, //press F12 - delete: false, //press delete - C: false, - X: false, - V: false, - O: false, - I: false -}; - -$(document) - .keydown(function (event) { - if (webDraft.selectedTool !== TEXT && $('#resizer').is('hidden')) { - event.preventDefault(); - } - - switch (event.keyCode) { - case 13 : - keys.Enter = true; - break; - case 27 : - keys.Esc = true; - break; - case 46 : - keys.delete = true; - break; - case 67 : - keys.C = true; - break; - case 73 : - keys.I = true; - break; - case 79 : - keys.O = true; - break; - case 83 : - keys.S = true; - break; - case 86 : - keys.V = true; - break; - case 88 : - keys.X = true; - break; - case 122 : - keys.f11 = true; - break; - case 123 : - keys.f12 = true; - break; - } - if (keys.delete) { - if (webDraft.selectedTool === SELECT) { - select.delSelectedPart(); - } - if (event.ctrlKey) { - webDraft.clear(); - } - } - - if (keys.f12 || keys.f11 || event.ctrlKey || keys.delete) { - event.preventDefault(); - } - - if (keys.C) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.copySelectedPart(); - } - } - - if (keys.I) { - if (event.ctrlKey) { - $("#info").toggle(); - } - } - - if (keys.X) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.cutSelectedPart(); - } - } - - if (keys.V) { - if (webDraft.selectedTool === SELECT && event.ctrlKey) { - select.pasteSelectedPart(); - } - } - if (keys.O) { - if (event.ctrlKey) { - $("#fileUploader").click(); - } - } - if (keys.S) { - if (event.ctrlKey) { - file.download(); - } - } - - }) - .keyup(function (event) { - switch (event.keyCode) { - case 13 : - keys.Enter = false; - break; - case 27 : - keys.Esc = false; - break; - case 46 : - keys.delete = false; - break; - case 67 : - keys.C = false; - break; - case 73 : - keys.I = false; - break; - case 79 : - keys.O = false; - break; - case 83 : - keys.S = false; - break; - case 86 : - keys.V = false; - break; - case 88 : - keys.X = false; - break; - case 122 : - keys.f11 = false; - break; - case 123 : - keys.f12 = false; - break; - } - }); - -$(window).bind('mousewheel DOMMouseScroll', function (event) { - if (event.ctrlKey === true) { - event.preventDefault(); - - if (event.originalEvent.wheelDelta / 120 > 0) { - if (webDraft.size < 250) { - webDraft.size += 2; - } else { - webDraft.size = 250; - } - } else { - if (webDraft.size > 1) { - webDraft.size -= 2; - } else { - webDraft.size = 1; - } - } - $("#eraseRect").css({ - "width": webDraft.size, - "height": webDraft.size, - "top": event.pageY - (webDraft.size / 2) + "px", - "left": event.pageX - (webDraft.size / 2) + "px" - }); - $("input#pointSize").val(webDraft.size); - $("#pointSizeValue").text("size:" + webDraft.size + "px"); - } - - if (event.altKey === true) { - // event.preventDefault(); - } -}); \ No newline at end of file diff --git a/js/start.js b/js/start.js deleted file mode 100644 index 17c293d..0000000 --- a/js/start.js +++ /dev/null @@ -1,141 +0,0 @@ -const DEBUG = 0; - -const PENCIL = 'pencil'; -const SELECT = 'select'; -const ERASER = 'eraser'; -const WEB = 'web'; -const TEXT = 'text'; -const RECTANGLE = 'rectangle'; -const CIRCLE = 'circle'; -const COLORSAMPLER = 'colorsampler'; - - -let items = [ - { - text: 'Clear', - shortcut: 'ctrl+del', - icon: 'fas fa-ban', - onclick: function () { - webDraft.clear(); - } - }, - { - text: 'Upload image', - shortcut: 'ctrl+o', - icon: 'fas fa-upload', - onclick: function () { - $('#fileUploader').click(); - } - }, - { - text: 'Save image', - shortcut: 'ctrl+s', - icon: 'fas fa-download', - onclick: function () { - file.download(); - } - }, - { - text: 'Color', - icon: 'fas fa-tint', - submenu: [ - { - text: 'Invert', - icon: 'fas fa-adjust', - onclick: function () { - layers.negative(); - } - }, - { - text: 'Colorpicker', - icon: 'fas fa-eye-dropper', - onclick: function () { - $('#colorsampler').click(); - } - } - ] - } -]; - -let context_menu = new Contextmenu(items); -let camera = new Camera(); -let file = new File(); -let shapes = new Shapes(); -let draw = new Draw(); -let text = new Text(); -let resizer = new Resizer(); -let layers = new Layers(); -let select = new Select(); -let webDraft = new WebDraft(); - - -let canvas, - ctx, - randomId, - points = {}; - - -$(window) - .resize(function () { - webDraft.resize(); - webDraft.positionElements(); - }); -$(document) - .ready(function (event) { - if (/mobile/i.test(navigator.userAgent)) { - webDraft.draw.width = window.innerWidth - 10; - webDraft.draw.height = window.innerHeight - 30; - } - - webDraft.init(); - $("#selectRectangle, #textRectangle") - .draggable({snap: false}) - .css({"position": "absolute"}); - - $("#shadowDot").draggable({ - containment: "#shadowSquare", - scroll: false, - drag: function () { - let shadowY = parseInt($(this).css('top')) - (parseInt($(this).parent().height()) / 2); - let shadowX = parseInt($(this).css('left')) - (parseInt($(this).parent().width()) / 2); - webDraft.shadow.offsetX = shadowX; - webDraft.shadow.offsetY = shadowY; - } - }); - $('#shadowSquare').on('mousedown', function (e) { - let x = e.pageX - $(this).offset().left; - let y = e.pageY - $(this).offset().top; - - $("#shadowDot").css({ - top: (y - 5) + 'px', - left: (x - 5) + 'px' - }); - - let shadowY = parseInt($("#shadowDot").css('top')) - (parseInt($("#shadowDot").parent().height()) / 2); - let shadowX = parseInt($("#shadowDot").css('left')) - (parseInt($("#shadowDot").parent().width()) / 2); - webDraft.shadow.offsetX = shadowX; - webDraft.shadow.offsetY = shadowY; - }); - - - }) - .bind("contextmenu", function (e) { - if (!DEBUG) { - e.preventDefault(); - } - }) - .on('mouseup touchend', function (e) { - webDraft._mouseup(e); - }) - .on('mousemove touchmove', function (e) { - webDraft._mousemove(e); - }); - -function hexToRgba(hex, opacity) { - hex = hex.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - const a = opacity / 100; - return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; -} \ No newline at end of file diff --git a/less/camera.less b/less/camera.less deleted file mode 100644 index 372eec9..0000000 --- a/less/camera.less +++ /dev/null @@ -1,181 +0,0 @@ -@import 'variables'; - -#camera { - box-shadow: 0px 0px 5px 2px #6f6f6f; - width: 740px; - max-width: 100vw; - height: 560px; - max-height: 100vh; - background: @dark3; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - position: absolute; - z-index: 99; - display: none; - &.opened { - display: block; - } - &.fullscreen { - width: 100vw; - height: 100vh; - top: 0; - left: 0; - transform: translate(0, 0); - .cameraContent { - .videoContainer { - width: 100vw; - height: ~"calc(100vh - 130px)"; - } - .filters { - width: 100vw; - height: 50px; - line-height: 50px; - label { - span.filter { - margin: 15px 5px; - } - } - } - } - } - #cameraHeader { - width: 100%; - height: 30px; - #cameraTitle { - width: ~"calc(100% - 70px)"; - float: left; - height: 30px; - line-height: 30px; - padding: 0px 5px; - color: @white1; - } - button { - width: 30px; - height: 30px; - border: none; - display: inline-block; - cursor: pointer; - float: right; - i { - cursor: pointer; - } - &#closeCamera { - background: @red1; - color: @white1; - } - &#switchFullscreen { - background: @blue2; - color: @white1; - margin-right: 10px; - } - } - } - .cameraContent { - width: 100%; - float: left; - margin-top: 10px; - .videoContainer { - width: ~"calc(100% - 100px)"; - float: left; - position: relative; - canvas#snapImage, - canvas#frame, - canvas#additional, - video { - width: 100%; - max-width: 100vw; - max-height: ~"calc(100vh - 130px)"; - float: left; - } - canvas#snapImage, - canvas#frame { - display: none; - } - canvas#additional { - display: block; - position: absolute; - top: 0; - left: 0; - } - } - .filters { - width: 100px; - float: left; - label { - input { - display: none; - &:checked + span { - background: @blue1; - color: @white1 - } - } - span.filter { - width: 70px; - height: 20px; - margin: 0px 15px 5px; - cursor: pointer; - background: @white2; - line-height: 20px; - text-align: center; - font-size: 13px; - float: left; - } - } - } - #cameraButtons { - float: left; - width: 100%; - height: 50px; - line-height: 50px; - button { - min-width: 50px; - border: none; - display: none; - cursor: pointer; - * { - cursor: pointer; - } - &#snap { - display: inline-block; - } - &#applySnap { - background: @green2; - color: @white1; - } - &#cancelSnap { - background: @red1; - color: @white1; - } - &#saveSnapOnComputer { - background: @blue1; - color: @white1; - } - } - } - - } - - &.snapped { - - #cameraButtons { - button { - display: inline-block; - &#snap { - display: none; - } - } - } - .cameraContent { - .videoContainer { - canvas#additional, - video#video { - display: none; - } - canvas#snapImage { - display: block; - } - } - } - } -} \ No newline at end of file diff --git a/less/contextmenu.less b/less/contextmenu.less deleted file mode 100644 index 2bc2793..0000000 --- a/less/contextmenu.less +++ /dev/null @@ -1,118 +0,0 @@ -@import "variables"; - -menu#contextmenu{ - display: none; - width: 230px; - height: auto; - position: fixed; - top:50%; - left:50%; - background: #fff; - z-index:10; - ul{ - li{ - cursor: pointer; - float: left; - height: 30px; - line-height: 30px; - position: relative; - width: 100%; - background: #fff; - color: @black1; - *{ - cursor: pointer; - } - .content{ - width: 100%; - float: left; - span{ - height: inherit; - float: left; - &.icon{ - width: 20px; - i{ - float: left; - width: 20px; - height: 30px; - line-height: 30px; - } - } - &.text{ - width: ~"calc(100% - 130px)"; - } - &.shortcut{ - text-align: right; - text-transform: uppercase; - width: 70px; - font-size: 11px; - color: @dark2; - } - &.arrow{ - color: @dark2; - width: 10px; - margin: 0px 5px; - font-size: 11px; - visibility: hidden; - opacity: 0; - } - } - } - .submenu{ - width: 230px; - position: absolute; - display: none; - top: 0; - left: 230px; - &+.content{ - span{ - &.arrow{ - &.left-arrow{ - visibility: hidden; - opacity: 0; - } - &.right-arrow{ - visibility: visible; - opacity: 1; - float: right; - } - } - } - } - } - &.disabled{ - color: @gray1; - } - &:not(.disabled):hover{ - color: @blue1; - background-color: @white6; - .submenu{ - display: block; - color: @black1; - } - } - } - } - &.left-submenu{ - ul{ - li{ - .submenu{ - left: -230px; - &+.content{ - span{ - &.arrow{ - &.left-arrow{ - visibility: visible; - opacity: 1; - } - &.right-arrow{ - visibility: hidden; - opacity: 0; - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/less/hints.less b/less/hints.less deleted file mode 100644 index 0561830..0000000 --- a/less/hints.less +++ /dev/null @@ -1,43 +0,0 @@ -//hints -@import "variables"; - - -#hint{ - width: 100vw; - height: 50px; - position: absolute; - bottom: 20px; - left: 0; - background: @dark3; - display: none; - .hintGroup{ - width: 100%; - height: 100%; - float: left; - display: none; - .keyboardButton{ - float: left; - padding: 0 5px; - min-width: 30px; - height: 20px; - background: @white5; - box-sizing: border-box; - border: 1px solid @gray3; - border-radius: 2px; - margin: 15px 5px; - color: @dark3; - text-align: center; - line-height: 20px; - font-size: 12px; - } - .describeHint{ - width: auto; - float: left; - height: 50px; - line-height: 50px; - margin: 0 5px 0 0; - color: @white4; - font-size: 12px; - } - } -} \ No newline at end of file diff --git a/less/info.less b/less/info.less deleted file mode 100644 index 657577c..0000000 --- a/less/info.less +++ /dev/null @@ -1,63 +0,0 @@ -//info -@import "variables"; - -#info{ - width: 350px; - height: 100vh; - z-index: 5; - position: absolute; - display: none; - overflow: auto; - background: @dark3; - border-left: 1px solid @gray2; - color: @white1; - top: 0px; - right: 0px; - @media all and (max-width: 400px){ - width: 100vw; - } - .close-info { - position: absolute; - right: 0; - cursor: pointer; - margin: 5px; - } - .info-section { - width: 100%; - float: left; - margin: 5px 0px; - .info-section-title { - width: 100%; - float: left; - text-align: center; - } - .info-section-line { - width: 100%; - float: left; - .info-section-shortcut { - width: 30%; - float: left; - padding: 2px 5px; - text-align: right; - span { - padding: 0 5px; - min-width: 30px; - height: 20px; - border-radius: 2px; - margin: 15px 5px; - text-align: center; - line-height: 20px; - font-size: 12px; - background: @white5; - border: 1px solid @gray3; - color: @dark3; - } - } - .info-section-shortcut-describe { - width: 70%; - float: left; - padding: 2px 5px; - } - } - } -} \ No newline at end of file diff --git a/less/inputs.less b/less/inputs.less deleted file mode 100644 index 54b23f4..0000000 --- a/less/inputs.less +++ /dev/null @@ -1,149 +0,0 @@ -//inputs -@import "variables"; - -input{ - &[type=checkbox]{ - &+label{ - cursor: pointer; - color: @gray2; - width: 100%; - background: transparent; - text-align: left; - border: none; - border-radius: 0px; - .fa-circle, .fa-check-circle{ - float: left; - text-align: center; - width: 30px; - vertical-align: middle; - margin: 0px 0px 0px 10px; - font-size: 18px; - height: 20px; - line-height: 20px; - } - .fa-check-circle{ - display: none; - } - #l{ - margin-left: 10px; - height: 20px; - line-height: 20px; - } - span{ - width: 100%; - height: 20px; - text-align: left; - &.label{ - width: auto; - } - } - } - &:checked{ - &+label{ - font-weight: bold; - color: @blue1; - .fa-circle{ - display: none; - } - .fa-check-circle{ - display: block; - } - } - } - &:hover{ - &+label{ - color: @white1; - .fa-circle{ - display: none; - } - .fa-check-circle{ - display: block; - } - } - } - } - &[type=range]{ - -webkit-appearance: none; - width: calc(~"100% - 20px"); - margin: 0 10px; - &::-webkit-slider-runnable-track{ - width: 230px; - height: 10px; - background: @white3; - border: none; - border-radius: 0px; - } - &::-webkit-slider-thumb{ - -webkit-appearance: none; - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: @blue1; - } - &::-moz-range-track{ - width: 230px; - height: 10px; - background: @white3; - border: none; - } - &::-moz-range-thumb{ - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: @blue1; - } - &::-ms-track{ - width: 230px; - height: 10px; - background: transparent; - border-color: transparent; - border-width: 0; - color: transparent; - } - &::-ms-thumb{ - border: none; - height: 10px; - width: 10px; - border-radius: 0px; - background: @blue1; - } - &:-moz-focusring{ - outline: none; - } - &::-ms-fill-lower{ - background: @white3; - } - &::-ms-fill-upper{ - background: @white3; - } - &:focus{ - outline: none; - &::-ms-fill-lower{ - background: @gray1; - } - &::-webkit-slider-runnable-track{ - background: @gray3; - } - &::-ms-fill-upper{ - background: @gray3; - } - } - } - &[type=color]{ - visibility: hidden; - width: 0px; - height: 0px; - position: absolute; - } - &#fileUploaderInput{ - display: none; - } - &#isFillSet{ - display: none; - } - &#isShadow{ - display: none; - } -} \ No newline at end of file diff --git a/less/layers.less b/less/layers.less deleted file mode 100644 index 449672c..0000000 --- a/less/layers.less +++ /dev/null @@ -1,110 +0,0 @@ -//layers -@import "variables"; - -#layers { - .title{ - width: 100%; - height: 20px; - line-height: 20px; - float: left; - font-size: 15px; - text-align: center; - background: @white2; - color: @dark2; - } - #layersBtns{ - width: 100%; - background: @dark3; - float: left; - z-index: 4; - color: @gray2; - .layerTool{ - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: @gray2; - cursor: pointer; - &#addLayer{ - color: @green1; - } - &#delLayer{ - color: @red1; - } - &:hover{ - color: @white1; - } - .fas, .fab, .far{ - font-size: 15px; - } - } - } - #listLayers{ - width: 100%; - background: @dark3; - float: left; - z-index: 4; - color: @gray2; - max-height: calc(~"50vh - 170px"); - overflow: auto; - top: 50px; - .layerView{ - position: relative; - width: 100%; - height: 60px; - margin: 10px auto; - background: transparent; - text-align: center; - text-align: center; - cursor: pointer; - &.active { - cursor: default; - background-color: @rgba1; - &:hover{ - cursor: default; - background-color: @rgba1; - } - .imgLayerContainer{ - border-color: @blue2; - } - } - &:hover { - background-color: @rgba2; - } - .imgLayerContainer{ - float: left; - border: 1px solid @gray3; - width: 60px; - height: 60px; - img.imgLayer{ - float: left; - max-width: 58px; - max-height: 58px; - } - } - .hideLayer, .showLayer{ - float: left; - margin: 21px 5px; - width: 18px; - height: 18px; - line-height: 18px !important; - text-align: center; - cursor: pointer; - } - - .showLayer{ - color: @red1; - &:hover{ - color: @green1; - } - } - .hideLayer{ - color: @green1; - &:hover{ - color: @red1; - } - } - } - } -} \ No newline at end of file diff --git a/less/resizer.less b/less/resizer.less deleted file mode 100644 index e55d6cd..0000000 --- a/less/resizer.less +++ /dev/null @@ -1,50 +0,0 @@ -//resizer -@import "variables"; - - -#resizer{ - width: 300px; - height: 210px; - position: absolute; - top: 0; - left: 0; - background: @dark3; - display: none; - padding: 10px; - z-index: 4; - margin-left: calc(~"(100vw - 300px) / 2"); - margin-top: calc(~"(100vh - 210px) / 2"); - input[type=number]{ - width: 250px; - height: 30px; - margin: 10px 15px; - border: none; - cursor: text; - text-align: center; - float: left; - } - #resizeinfo{ - width: 250px; - height: 30px; - line-height: 30px; - text-align: center; - color: @gray3; - float: left; - margin: 0px 20px; - } - .buttonAction{ - float: left; - width: 120px; - height: 30px; - line-height: 30px; - font-size: 15px; - margin: 10px 10px; - text-align: center; - background: @gray3; - cursor: pointer; - &:hover{ - background: @blue1; - color: @gray3; - } - } -} diff --git a/less/shadow-changer.less b/less/shadow-changer.less deleted file mode 100644 index b7c926d..0000000 --- a/less/shadow-changer.less +++ /dev/null @@ -1,41 +0,0 @@ -//shadow-changer -@import "variables"; - - -#shadowSquare{ - width: 200px; - height: 200px; - position: relative; - background: @rgba3; - float: left; - display: none; - #shadowDot{ - cursor: move; - background: @white1; - border-radius: 100%; - width: 10px; - height: 10px; - position: absolute; - top: 95px; - left: 95px; - z-index: 2; - } - &::after, &::before{ - position: absolute; - content: ' '; - background: @gray2; - z-index: 1; - } - &::after{ - width: 200px; - height: 1px; - top: 50%; - left: 0px; - } - &::before{ - width: 1px; - height: 200px; - top: 0%; - left: 50%; - } -} \ No newline at end of file diff --git a/less/sliders.less b/less/sliders.less deleted file mode 100644 index a52fe6e..0000000 --- a/less/sliders.less +++ /dev/null @@ -1,31 +0,0 @@ -//sliders -@import "variables"; - -#sliders{ - width: 100%; - float: left; - .sliderContainer{ - width: 100%; - height: 45px; - float: left; - &#shadow_slider{ - display: none; - } - &#sensitivityPoints_slider{ - display: none; - } - &#fillOpacity_slider{ - display: none; - } - .sliderVal{ - width: 100%; - height: 20px; - margin-top: 5px; - line-height: 20px; - word-wrap: break-word; - font-size: 13px; - float: left; - text-align: center; - } - } -} \ No newline at end of file diff --git a/less/statusbar.less b/less/statusbar.less deleted file mode 100644 index 2a6db74..0000000 --- a/less/statusbar.less +++ /dev/null @@ -1,32 +0,0 @@ -//statusbar -@import "variables"; - -#statusbar{ - width: 100%; - background: @dark3; - height: 20px; - line-height: 20px; - float: left; - position: absolute; - bottom: 0; - font-size: 13px; - color: @gray3; - #mousePosition{ - float: left; - width: 100px; - height: inherit; - line-height: inherit; - text-align: center; - } - #author{ - height: inherit; - line-height: inherit; - float: right; - right: 0; - margin-right: 20px; - a{ - color: @white2; - cursor: pointer; - } - } -} \ No newline at end of file diff --git a/less/style.less b/less/style.less deleted file mode 100644 index 5fac498..0000000 --- a/less/style.less +++ /dev/null @@ -1,92 +0,0 @@ -@import "variables"; -@import "inputs"; -@import "tools"; -@import "text-options"; -@import "layers"; -@import "sliders"; -@import "info"; -@import "resizer"; -@import "hints"; -@import "shadow-changer"; -@import "statusbar"; -@import "camera"; -@import "contextmenu"; - -@import url(https://fonts.googleapis.com/css?family=Open+Sans); -*{ - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: default; - font-family: 'Open Sans', sans-serif; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - margin: 0; - padding: 0; -} -ul{ - list-style: none; - -} -html{ - body{ - top: 0; - left: 0; - width: 100%; - height: 100%; - float: left; - position: fixed; - overflow: hidden; - #content{ - float: left; - background: @bg_color; - overflow: auto; - width: calc(~"100vw - 200px"); - height: calc(~"100vh - 20px"); - #drawHandler{ - border: 1px dotted @blue1; - position: relative; - overflow: hidden; - #draw, #drawHandler, #eventHandler{ - width: 600px; - height: 400px; - overflow: hidden; - position: relative; - /*cursor: crosshair;*/ - } - #eventHandler{ - z-index: 3; - position: absolute; - } - #draw{ - canvas{ - position: absolute; - cursor: crosshair; - border: 1px dotted #0052CC; - &.invisible{ - visibility: hidden; - } - } - } - } - } - #eraseRect, - #prepareRect, - #prepareCircle, - #selectRectangle{ - display: none; - position: absolute; - z-index: 2; - border: 1px solid @black1; - } - #selectRectangle{ - cursor: move; - } - #eraseRect{ - border-radius: 100%; - border-color: @gray3; - } - } -} \ No newline at end of file diff --git a/less/text-options.less b/less/text-options.less deleted file mode 100644 index e1a66f6..0000000 --- a/less/text-options.less +++ /dev/null @@ -1,53 +0,0 @@ -//text-options -@import "variables"; - -#textOptions{ - display: none; - #alignBtns{ - width: 100%; - float: left; - .textPostionTool{ - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: @gray2; - cursor: pointer; - .fas, .fab, .far{ - font-size: 15px; - } - &:hover, &.active{ - color: @white1; - } - } - } - #fontStyleBtns{ - width: 100%; - float: left; - .styleTool{ - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: @gray2; - cursor: pointer; - .fas, .fab, .far{ - font-size: 15px; - } - &:hover, &.active{ - color: @white1; - } - } - } -} - -#textRectangle{ - display: none; - position: absolute; - z-index: 2; - border: 1px solid @black1; - font-size: 8mm; - cursor: move; -} \ No newline at end of file diff --git a/less/tools.less b/less/tools.less deleted file mode 100644 index 69afc57..0000000 --- a/less/tools.less +++ /dev/null @@ -1,150 +0,0 @@ -//tools -@import "variables"; - -#toolsGroup{ - width: 200px; - height: calc(~"100vh - 20px"); - background: @dark3; - float: left; - #previewColorSampler{ - width: 100%; - height: 60px; - background: 0 30px url('/pic/transparent.png'); - z-index: 1; - display: none; - #textColorSampler{ - width: 100%; - float: left; - height: 30px; - background: @dark3; - line-height: 30px; - text-align: center; - font-size: 13px; - color: @white2; - } - #colorBoxSampler{ - width: 100%; - height: 30px; - float: left; - } - } - .tools{ - width: 100%; - background: @dark3; - float: left; - z-index: 4; - color: @gray2; - .title{ - width: 100%; - height: 20px; - line-height: 20px; - float: left; - font-size: 15px; - text-align: center; - background: @white2; - color: @dark2; - .fas, .fab, .far{ - position: relative; - height: inherit; - line-height: inherit; - left: 0; - top: 0; - text-align: center; - } - } - #toolsBtns{ - width: 100%; - float: left; - .primaryTool, - .paintTools{ - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - float: left; - color: @gray2; - cursor: pointer; - .fas, .fab, .far{ - font-size: 15px; - } - &:hover{ - color: @white1; - } - } - .paintTools{ - &.active{ - cursor: default; - color: @blue1; - background: @dark1; - } - } - .primaryTool{ - &:active{ - cursor: default; - color: @blue1; - background: @dark1; - } - } - } - #radioBtns{ - width: 100%; - float: left; - #shadowInput, #fillShapeInput{ - width: 100%; - line-height: 20px; - float: left; - padding: 5px 0px; - font-size: 12px; - *{ - cursor: pointer; - float: left; - } - } - #fillShapeInput{ - display: none; - } - } - #colorPicker{ - width: 100%; - float: left; - .colorTab{ - width: 100%; - height: 25px; - line-height: 25px; - font-size: 14px; - cursor: pointer; - .color{ - border-radius: 100%; - width: 15px; - height: 15px; - margin: 5px 10px; - float: left; - border: 1px solid @gray3; - } - &#generalColor{ - .color{ - background: #000000; - } - } - &#shadowColor{ - display: none; - .color{ - background: #232324; - } - } - &#fillColor{ - display: none; - .color{ - background: #FFFFFF; - } - } - &:hover { - color: @blue1; - background: @dark1; - } - } - } - - - } -} \ No newline at end of file diff --git a/less/variables.less b/less/variables.less deleted file mode 100644 index 3af0bfd..0000000 --- a/less/variables.less +++ /dev/null @@ -1,31 +0,0 @@ -//variables -@bg_color: #373636; - - -@black1: #000000; - -@white1: #FFFFFF; -@white2: #EEEEEE; -@white3: #DDDDDD; -@white4: #F1F1E4; -@white5: #F1F1F1; -@white6: #dedede; - -@dark1: #1C1B1B; -@dark2: #2E3436; -@dark3: #2C2B2B; - -@gray1: #888888; -@gray2: #A9A9A9; -@gray3: #CCCCCC; - -@blue1: #0052CC; -@blue2: #0021CC; - -@red1: #C50000; -@green1: #16c600; -@green2: #129a01; - -@rgba1: rgba(0,0,0,0.1); -@rgba2: rgba(0,0,0,0.3); -@rgba3: rgba(0,0,0,0.4); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1be093f..0d31313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1547 +1,1002 @@ { - "name": "webdraft", - "version": "2.3.3", - "lockfileVersion": 1, + "name": "webdraft-v3", + "version": "0.1.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "packages": { + "": { + "name": "webdraft-v3", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.5.4", + "vite": "^5.4.0" } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "requires": { - "sprintf-js": "1.0.3" - } - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "optional": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "optional": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "optional": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "optional": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "license": "MIT", "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "requires": { - "continuable-cache": "0.3.1", - "error": "7.0.2", - "raw-body": "1.1.7", - "safe-json-parse": "1.0.1" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", - "requires": { - "source-map": "0.5.7" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "optional": true - }, - "coffeescript": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", - "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "boom": "2.10.1" - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "1.0.2" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - } - } - }, - "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "dev": true, - "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "prr": "1.0.1" - } - }, - "error": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", - "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", - "requires": { - "string-template": "0.2.1", - "xtend": "4.0.1" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, - "requires": { - "is-arrayish": "0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "optional": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "requires": { - "websocket-driver": "0.7.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true, - "requires": { - "glob": "5.0.15" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "1.2.0" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - } - } - }, - "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "globule": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", - "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", - "requires": { - "glob": "7.1.2", - "lodash": "4.17.10", - "minimatch": "3.0.4" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "grunt": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.2.tgz", - "integrity": "sha1-TmpeaVtwRy/VME9fqeNCNoNqc7w=", - "dev": true, - "requires": { - "coffeescript": "1.10.0", - "dateformat": "1.0.12", - "eventemitter2": "0.4.14", - "exit": "0.1.2", - "findup-sync": "0.3.0", - "glob": "7.0.6", - "grunt-cli": "1.2.0", - "grunt-known-options": "1.1.0", - "grunt-legacy-log": "1.0.2", - "grunt-legacy-util": "1.0.0", - "iconv-lite": "0.4.23", - "js-yaml": "3.5.5", - "minimatch": "3.0.4", - "nopt": "3.0.6", - "path-is-absolute": "1.0.1", - "rimraf": "2.2.8" - }, - "dependencies": { - "grunt-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", - "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", - "dev": true, - "requires": { - "findup-sync": "0.3.0", - "grunt-known-options": "1.1.0", - "nopt": "3.0.6", - "resolve": "1.1.7" - } - } - } - }, - "grunt-contrib-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", - "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", - "requires": { - "chalk": "1.1.3", - "source-map": "0.5.7" - } - }, - "grunt-contrib-cssmin": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-2.2.1.tgz", - "integrity": "sha512-IXNomhQ5ekVZbDbj/ik5YccoD9khU6LT2fDXqO1+/Txjq8cp0tQKjVS8i8EAbHOrSDkL7/UD6A7b+xj98gqh9w==", - "requires": { - "chalk": "1.1.3", - "clean-css": "4.1.11", - "maxmin": "2.1.0" - } - }, - "grunt-contrib-less": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.1.tgz", - "integrity": "sha1-O73sC3XRLOqlXWKUNiXAsIYc328=", - "requires": { - "async": "2.6.1", - "chalk": "1.1.3", - "less": "2.7.3", - "lodash": "4.17.10" - }, - "dependencies": { - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "4.17.10" - } - } - } - }, - "grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", - "requires": { - "async": "2.6.1", - "gaze": "1.1.3", - "lodash": "4.17.10", - "tiny-lr": "1.1.1" - }, - "dependencies": { - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "4.17.10" - } - } - } - }, - "grunt-known-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.0.tgz", - "integrity": "sha1-pCdO6zL6dl2lp6OxcSYXzjsUQUk=", - "dev": true - }, - "grunt-legacy-log": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-1.0.2.tgz", - "integrity": "sha512-WdedTJ/6zCXnI/coaouzqvkI19uwqbcPkdsXiDRKJyB5rOUlOxnCnTVbpeUdEckKVir2uHF3rDBYppj2p6N3+g==", - "dev": true, - "requires": { - "colors": "1.1.2", - "grunt-legacy-log-utils": "1.0.0", - "hooker": "0.2.3", - "lodash": "4.17.10" - } - }, - "grunt-legacy-log-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-1.0.0.tgz", - "integrity": "sha1-p7ji0Ps1taUPSvmG/BEnSevJbz0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "lodash": "4.3.0" - }, - "dependencies": { - "lodash": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", - "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", - "dev": true - } - } - }, - "grunt-legacy-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz", - "integrity": "sha1-OGqnjcbtUJhsKxiVcmWxtIq7m4Y=", - "dev": true, - "requires": { - "async": "1.5.2", - "exit": "0.1.2", - "getobject": "0.1.0", - "hooker": "0.2.3", - "lodash": "4.3.0", - "underscore.string": "3.2.3", - "which": "1.2.14" - }, - "dependencies": { - "lodash": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", - "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", - "dev": true - } - } - }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", - "requires": { - "duplexer": "0.1.1" - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", - "dev": true - }, - "http-parser-js": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", - "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=" - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", - "optional": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "2.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true - }, - "js-yaml": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", - "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", - "dev": true, - "requires": { - "argparse": "1.0.10", - "esprima": "2.7.3" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "optional": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - } - } - }, - "less": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", - "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", - "requires": { - "errno": "0.1.7", - "graceful-fs": "4.1.11", - "image-size": "0.5.5", - "mime": "1.6.0", - "mkdirp": "0.5.1", - "promise": "7.3.1", - "request": "2.81.0", - "source-map": "0.5.7" - } - }, - "livereload-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz", - "integrity": "sha512-j1R0/FeGa64Y+NmqfZhyoVRzcFlOZ8sNlKzHjh4VvLULFACZhn68XrX5DFg2FhMvSMJmROuFxRSa560ECWKBMg==" - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "material-design-icons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", - "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=" - }, - "maxmin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", - "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", - "requires": { - "chalk": "1.1.3", - "figures": "1.7.0", - "gzip-size": "3.0.0", - "pretty-bytes": "3.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "optional": true - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1.1.1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "2.6.0", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "1.3.1" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", - "optional": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pretty-bytes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", - "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "asap": "2.0.6" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "optional": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "optional": true - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "requires": { - "bytes": "1.0.0", - "string_decoder": "0.10.31" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" - } - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.2", - "stringstream": "0.0.6", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, - "requires": { - "hoek": "2.16.3" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "dev": true + "license": "MIT" }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, - "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "4.0.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "tiny-lr": { + "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "requires": { - "body": "5.1.0", - "debug": "3.1.0", - "faye-websocket": "0.10.0", - "livereload-js": "2.3.0", - "object-assign": "4.1.1", - "qs": "6.4.0" - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, - "requires": { - "safe-buffer": "5.1.2" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "underscore.string": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.2.3.tgz", - "integrity": "sha1-gGmSYzZl1eX8tNsfs6hi62jp5to=", - "dev": true - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "optional": true - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", "dev": true, - "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { "optional": true } } - }, - "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", - "requires": { - "http-parser-js": "0.4.13", - "websocket-extensions": "0.1.3" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" - }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } } diff --git a/package.json b/package.json index 3e478c0..e5381fb 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,17 @@ { - "name": "webdraft", - "version": "2.3.3", - "description": "Web application for drawing.", - "main": "js/start.js", + "name": "webdraft-v3", + "version": "0.1.0", + "private": true, + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "vite --host 127.0.0.1", + "test": "node tests/layer-transforms.test.mjs && node tests/editor-geometry.test.mjs && node tests/history-manager.test.mjs && node tests/flood-fill.test.mjs", + "check": "npm test && npm run build", + "build": "tsc && vite build", + "preview": "vite preview --host 127.0.0.1" }, - "repository": { - "type": "git", - "url": "git+https://github.com/rtcoder/WebDraft.git" - }, - "author": "Dawid Jeż (rtcoder)", - "license": "MIT", - "bugs": { - "url": "https://github.com/rtcoder/WebDraft/issues" - }, - "homepage": "https://github.com/rtcoder/WebDraft#readme", "devDependencies": { - "grunt": "^1.0.2" - }, - "dependencies": { - "grunt-contrib-concat": "^1.0.1", - "grunt-contrib-cssmin": "^2.2.1", - "grunt-contrib-less": "^1.4.1", - "grunt-contrib-watch": "^1.1.0" + "typescript": "^5.5.4", + "vite": "^5.4.0" } } diff --git a/parts/buttons.part.html b/parts/buttons.part.html deleted file mode 100644 index 9d25fbd..0000000 --- a/parts/buttons.part.html +++ /dev/null @@ -1,42 +0,0 @@ -
      - - Tools -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      - - -
      -
      diff --git a/parts/camera.part.html b/parts/camera.part.html deleted file mode 100644 index f35cc39..0000000 --- a/parts/camera.part.html +++ /dev/null @@ -1,38 +0,0 @@ -
      -
      - - -
      -
      -
      - - - - -
      -
      - - - - -
      -
      - - - - -
      -
      \ No newline at end of file diff --git a/parts/color-picker.part.html b/parts/color-picker.part.html deleted file mode 100644 index 027df72..0000000 --- a/parts/color-picker.part.html +++ /dev/null @@ -1,15 +0,0 @@ - -
      -
      -
      - Color -
      -
      -
      - Shadow color -
      -
      -
      - Fill color -
      -
      \ No newline at end of file diff --git a/parts/info.part.html b/parts/info.part.html deleted file mode 100644 index b4d3dfe..0000000 --- a/parts/info.part.html +++ /dev/null @@ -1,61 +0,0 @@ -
      -
      -
      General
      -
      -
      - CTRL + I -
      -
      Show/hide this info
      -
      -
      -
      - CTRL + S -
      -
      Save image
      -
      -
      -
      - CTRL + O -
      -
      Upload image
      -
      -
      -
      - CTRL + DEL -
      -
      Clear image
      -
      -
      -
      - CTRL + Scroll -
      -
      Pencil size
      -
      -
      -
      -
      Selection
      -
      -
      - CTRL + X -
      -
      Cut
      -
      -
      -
      - CTRL + C -
      -
      Copy
      -
      -
      -
      - CTRL + V -
      -
      Paste
      -
      -
      -
      - DEL -
      -
      Delete selection
      -
      -
      \ No newline at end of file diff --git a/parts/layers.part.html b/parts/layers.part.html deleted file mode 100644 index acd02d7..0000000 --- a/parts/layers.part.html +++ /dev/null @@ -1,11 +0,0 @@ -
      - - Layers -
      -
      -
      -
      -
      -
      -
      -
      \ No newline at end of file diff --git a/parts/resizer.part.html b/parts/resizer.part.html deleted file mode 100644 index 16bc7dc..0000000 --- a/parts/resizer.part.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - -
      -
      Cancel (Esc)
      -
      OK (Enter)
      \ No newline at end of file diff --git a/parts/sliders.part.html b/parts/sliders.part.html deleted file mode 100644 index 45b1bd0..0000000 --- a/parts/sliders.part.html +++ /dev/null @@ -1,18 +0,0 @@ -
      -
      -
      size:10px
      - -
      -
      -
      shadow:1px
      - -
      -
      -
      sensitivity:10%
      - -
      -
      -
      fill opacity:100%
      - -
      -
      \ No newline at end of file diff --git a/parts/text-options.part.html b/parts/text-options.part.html deleted file mode 100644 index 780d52a..0000000 --- a/parts/text-options.part.html +++ /dev/null @@ -1,36 +0,0 @@ -
      -
      -
      - -
      -
      -
      -
      -
      -
      -
      -
      - - \ No newline at end of file diff --git a/pic/transparent.png b/pic/transparent.png deleted file mode 100644 index 01dc43dbe5d17ae671a74b8d993d6f4a41fa4b63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPqmzX#o)23r~UO*wq64!_l=ltB<)VvY~=c3falGGH1 z^30M91$R&1fbd2>aiAh?PZ!4!i_^(-EQ_Djv)Md&b93{LC6Y5FG*}k1@vt#Ra2(yd hapT7f1-^P_2KH2r;Mu#bmH@Rec)I$ztaD0e0svy*Hyi)} diff --git a/app-v3/src/core/canvas-drawing.ts b/src/core/canvas-drawing.ts similarity index 100% rename from app-v3/src/core/canvas-drawing.ts rename to src/core/canvas-drawing.ts diff --git a/app-v3/src/core/editor-geometry.ts b/src/core/editor-geometry.ts similarity index 100% rename from app-v3/src/core/editor-geometry.ts rename to src/core/editor-geometry.ts diff --git a/app-v3/src/core/flood-fill.ts b/src/core/flood-fill.ts similarity index 100% rename from app-v3/src/core/flood-fill.ts rename to src/core/flood-fill.ts diff --git a/app-v3/src/core/history-manager.ts b/src/core/history-manager.ts similarity index 100% rename from app-v3/src/core/history-manager.ts rename to src/core/history-manager.ts diff --git a/app-v3/src/core/i18n.ts b/src/core/i18n.ts similarity index 100% rename from app-v3/src/core/i18n.ts rename to src/core/i18n.ts diff --git a/app-v3/src/core/layer-manager.ts b/src/core/layer-manager.ts similarity index 100% rename from app-v3/src/core/layer-manager.ts rename to src/core/layer-manager.ts diff --git a/app-v3/src/core/layer-transforms.ts b/src/core/layer-transforms.ts similarity index 100% rename from app-v3/src/core/layer-transforms.ts rename to src/core/layer-transforms.ts diff --git a/app-v3/src/core/project-file.ts b/src/core/project-file.ts similarity index 100% rename from app-v3/src/core/project-file.ts rename to src/core/project-file.ts diff --git a/app-v3/src/core/types.ts b/src/core/types.ts similarity index 100% rename from app-v3/src/core/types.ts rename to src/core/types.ts diff --git a/app-v3/src/core/webdraft-editor.ts b/src/core/webdraft-editor.ts similarity index 100% rename from app-v3/src/core/webdraft-editor.ts rename to src/core/webdraft-editor.ts diff --git a/app-v3/src/i18n/en/camera.ts b/src/i18n/en/camera.ts similarity index 100% rename from app-v3/src/i18n/en/camera.ts rename to src/i18n/en/camera.ts diff --git a/app-v3/src/i18n/en/common.ts b/src/i18n/en/common.ts similarity index 100% rename from app-v3/src/i18n/en/common.ts rename to src/i18n/en/common.ts diff --git a/app-v3/src/i18n/en/edit.ts b/src/i18n/en/edit.ts similarity index 100% rename from app-v3/src/i18n/en/edit.ts rename to src/i18n/en/edit.ts diff --git a/app-v3/src/i18n/en/file.ts b/src/i18n/en/file.ts similarity index 100% rename from app-v3/src/i18n/en/file.ts rename to src/i18n/en/file.ts diff --git a/app-v3/src/i18n/en/image.ts b/src/i18n/en/image.ts similarity index 100% rename from app-v3/src/i18n/en/image.ts rename to src/i18n/en/image.ts diff --git a/app-v3/src/i18n/en/index.ts b/src/i18n/en/index.ts similarity index 100% rename from app-v3/src/i18n/en/index.ts rename to src/i18n/en/index.ts diff --git a/app-v3/src/i18n/en/layer.ts b/src/i18n/en/layer.ts similarity index 100% rename from app-v3/src/i18n/en/layer.ts rename to src/i18n/en/layer.ts diff --git a/app-v3/src/i18n/en/layers.ts b/src/i18n/en/layers.ts similarity index 100% rename from app-v3/src/i18n/en/layers.ts rename to src/i18n/en/layers.ts diff --git a/app-v3/src/i18n/en/menu.ts b/src/i18n/en/menu.ts similarity index 100% rename from app-v3/src/i18n/en/menu.ts rename to src/i18n/en/menu.ts diff --git a/app-v3/src/i18n/en/picker.ts b/src/i18n/en/picker.ts similarity index 100% rename from app-v3/src/i18n/en/picker.ts rename to src/i18n/en/picker.ts diff --git a/app-v3/src/i18n/en/saveDialog.ts b/src/i18n/en/saveDialog.ts similarity index 100% rename from app-v3/src/i18n/en/saveDialog.ts rename to src/i18n/en/saveDialog.ts diff --git a/app-v3/src/i18n/en/toolbar.ts b/src/i18n/en/toolbar.ts similarity index 100% rename from app-v3/src/i18n/en/toolbar.ts rename to src/i18n/en/toolbar.ts diff --git a/app-v3/src/i18n/pl/camera.ts b/src/i18n/pl/camera.ts similarity index 100% rename from app-v3/src/i18n/pl/camera.ts rename to src/i18n/pl/camera.ts diff --git a/app-v3/src/i18n/pl/common.ts b/src/i18n/pl/common.ts similarity index 100% rename from app-v3/src/i18n/pl/common.ts rename to src/i18n/pl/common.ts diff --git a/app-v3/src/i18n/pl/edit.ts b/src/i18n/pl/edit.ts similarity index 100% rename from app-v3/src/i18n/pl/edit.ts rename to src/i18n/pl/edit.ts diff --git a/app-v3/src/i18n/pl/file.ts b/src/i18n/pl/file.ts similarity index 100% rename from app-v3/src/i18n/pl/file.ts rename to src/i18n/pl/file.ts diff --git a/app-v3/src/i18n/pl/image.ts b/src/i18n/pl/image.ts similarity index 100% rename from app-v3/src/i18n/pl/image.ts rename to src/i18n/pl/image.ts diff --git a/app-v3/src/i18n/pl/index.ts b/src/i18n/pl/index.ts similarity index 100% rename from app-v3/src/i18n/pl/index.ts rename to src/i18n/pl/index.ts diff --git a/app-v3/src/i18n/pl/layer.ts b/src/i18n/pl/layer.ts similarity index 100% rename from app-v3/src/i18n/pl/layer.ts rename to src/i18n/pl/layer.ts diff --git a/app-v3/src/i18n/pl/layers.ts b/src/i18n/pl/layers.ts similarity index 100% rename from app-v3/src/i18n/pl/layers.ts rename to src/i18n/pl/layers.ts diff --git a/app-v3/src/i18n/pl/menu.ts b/src/i18n/pl/menu.ts similarity index 100% rename from app-v3/src/i18n/pl/menu.ts rename to src/i18n/pl/menu.ts diff --git a/app-v3/src/i18n/pl/picker.ts b/src/i18n/pl/picker.ts similarity index 100% rename from app-v3/src/i18n/pl/picker.ts rename to src/i18n/pl/picker.ts diff --git a/app-v3/src/i18n/pl/saveDialog.ts b/src/i18n/pl/saveDialog.ts similarity index 100% rename from app-v3/src/i18n/pl/saveDialog.ts rename to src/i18n/pl/saveDialog.ts diff --git a/app-v3/src/i18n/pl/toolbar.ts b/src/i18n/pl/toolbar.ts similarity index 100% rename from app-v3/src/i18n/pl/toolbar.ts rename to src/i18n/pl/toolbar.ts diff --git a/app-v3/src/i18n/types.ts b/src/i18n/types.ts similarity index 100% rename from app-v3/src/i18n/types.ts rename to src/i18n/types.ts diff --git a/app-v3/src/main.ts b/src/main.ts similarity index 100% rename from app-v3/src/main.ts rename to src/main.ts diff --git a/app-v3/src/styles.css b/src/styles.css similarity index 100% rename from app-v3/src/styles.css rename to src/styles.css diff --git a/app-v3/src/ui/camera-panel.ts b/src/ui/camera-panel.ts similarity index 100% rename from app-v3/src/ui/camera-panel.ts rename to src/ui/camera-panel.ts diff --git a/app-v3/src/ui/color-picker.ts b/src/ui/color-picker.ts similarity index 100% rename from app-v3/src/ui/color-picker.ts rename to src/ui/color-picker.ts diff --git a/app-v3/src/ui/keyboard-shortcuts.ts b/src/ui/keyboard-shortcuts.ts similarity index 100% rename from app-v3/src/ui/keyboard-shortcuts.ts rename to src/ui/keyboard-shortcuts.ts diff --git a/app-v3/src/ui/lang-state.ts b/src/ui/lang-state.ts similarity index 100% rename from app-v3/src/ui/lang-state.ts rename to src/ui/lang-state.ts diff --git a/app-v3/src/ui/layers-panel.ts b/src/ui/layers-panel.ts similarity index 100% rename from app-v3/src/ui/layers-panel.ts rename to src/ui/layers-panel.ts diff --git a/app-v3/src/ui/menu-bar.ts b/src/ui/menu-bar.ts similarity index 100% rename from app-v3/src/ui/menu-bar.ts rename to src/ui/menu-bar.ts diff --git a/app-v3/src/ui/resize-dialog.ts b/src/ui/resize-dialog.ts similarity index 100% rename from app-v3/src/ui/resize-dialog.ts rename to src/ui/resize-dialog.ts diff --git a/app-v3/src/ui/right-panel.ts b/src/ui/right-panel.ts similarity index 100% rename from app-v3/src/ui/right-panel.ts rename to src/ui/right-panel.ts diff --git a/app-v3/src/ui/save-dialog.ts b/src/ui/save-dialog.ts similarity index 100% rename from app-v3/src/ui/save-dialog.ts rename to src/ui/save-dialog.ts diff --git a/app-v3/src/ui/status-toasts.ts b/src/ui/status-toasts.ts similarity index 100% rename from app-v3/src/ui/status-toasts.ts rename to src/ui/status-toasts.ts diff --git a/app-v3/src/ui/text-toolbar.ts b/src/ui/text-toolbar.ts similarity index 100% rename from app-v3/src/ui/text-toolbar.ts rename to src/ui/text-toolbar.ts diff --git a/app-v3/src/ui/toolbar-controls.ts b/src/ui/toolbar-controls.ts similarity index 100% rename from app-v3/src/ui/toolbar-controls.ts rename to src/ui/toolbar-controls.ts diff --git a/app-v3/src/ui/toolbar-sections.ts b/src/ui/toolbar-sections.ts similarity index 100% rename from app-v3/src/ui/toolbar-sections.ts rename to src/ui/toolbar-sections.ts diff --git a/app-v3/src/ui/toolbar.ts b/src/ui/toolbar.ts similarity index 100% rename from app-v3/src/ui/toolbar.ts rename to src/ui/toolbar.ts diff --git a/app-v3/tests/editor-geometry.test.mjs b/tests/editor-geometry.test.mjs similarity index 100% rename from app-v3/tests/editor-geometry.test.mjs rename to tests/editor-geometry.test.mjs diff --git a/app-v3/tests/flood-fill.test.mjs b/tests/flood-fill.test.mjs similarity index 100% rename from app-v3/tests/flood-fill.test.mjs rename to tests/flood-fill.test.mjs diff --git a/app-v3/tests/history-manager.test.mjs b/tests/history-manager.test.mjs similarity index 100% rename from app-v3/tests/history-manager.test.mjs rename to tests/history-manager.test.mjs diff --git a/app-v3/tests/layer-transforms.test.mjs b/tests/layer-transforms.test.mjs similarity index 100% rename from app-v3/tests/layer-transforms.test.mjs rename to tests/layer-transforms.test.mjs diff --git a/app-v3/tests/test-support.mjs b/tests/test-support.mjs similarity index 100% rename from app-v3/tests/test-support.mjs rename to tests/test-support.mjs diff --git a/app-v3/tsconfig.json b/tsconfig.json similarity index 100% rename from app-v3/tsconfig.json rename to tsconfig.json diff --git a/vendor/canvas-to-blob/canvas-toBlob.js b/vendor/canvas-to-blob/canvas-toBlob.js deleted file mode 100644 index 6d895a7..0000000 --- a/vendor/canvas-to-blob/canvas-toBlob.js +++ /dev/null @@ -1,124 +0,0 @@ -/* canvas-toBlob.js - * A canvas.toBlob() implementation. - * 2013-12-27 - * - * By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr - * License: MIT - * See https://github.com/eligrey/canvas-toBlob.js/blob/master/LICENSE.md - */ - -/*global self */ -/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, - plusplus: true */ - -/*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */ - -(function(view) { -"use strict"; -var - Uint8Array = view.Uint8Array - , HTMLCanvasElement = view.HTMLCanvasElement - , canvas_proto = HTMLCanvasElement && HTMLCanvasElement.prototype - , is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i - , to_data_url = "toDataURL" - , base64_ranks - , decode_base64 = function(base64) { - var - len = base64.length - , buffer = new Uint8Array(len / 4 * 3 | 0) - , i = 0 - , outptr = 0 - , last = [0, 0] - , state = 0 - , save = 0 - , rank - , code - , undef - ; - while (len--) { - code = base64.charCodeAt(i++); - rank = base64_ranks[code-43]; - if (rank !== 255 && rank !== undef) { - last[1] = last[0]; - last[0] = code; - save = (save << 6) | rank; - state++; - if (state === 4) { - buffer[outptr++] = save >>> 16; - if (last[1] !== 61 /* padding character */) { - buffer[outptr++] = save >>> 8; - } - if (last[0] !== 61 /* padding character */) { - buffer[outptr++] = save; - } - state = 0; - } - } - } - // 2/3 chance there's going to be some null bytes at the end, but that - // doesn't really matter with most image formats. - // If it somehow matters for you, truncate the buffer up outptr. - return buffer; - } -; -if (Uint8Array) { - base64_ranks = new Uint8Array([ - 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1 - , -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - , 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 - , -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35 - , 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 - ]); -} -if (HTMLCanvasElement && !canvas_proto.toBlob) { - canvas_proto.toBlob = function(callback, type /*, ...args*/) { - if (!type) { - type = "image/png"; - } if (this.mozGetAsFile) { - callback(this.mozGetAsFile("canvas", type)); - return; - } if (this.msToBlob && /^\s*image\/png\s*(?:$|;)/i.test(type)) { - callback(this.msToBlob()); - return; - } - - var - args = Array.prototype.slice.call(arguments, 1) - , dataURI = this[to_data_url].apply(this, args) - , header_end = dataURI.indexOf(",") - , data = dataURI.substring(header_end + 1) - , is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)) - , blob - ; - if (Blob.fake) { - // no reason to decode a data: URI that's just going to become a data URI again - blob = new Blob - if (is_base64) { - blob.encoding = "base64"; - } else { - blob.encoding = "URI"; - } - blob.data = data; - blob.size = data.length; - } else if (Uint8Array) { - if (is_base64) { - blob = new Blob([decode_base64(data)], {type: type}); - } else { - blob = new Blob([decodeURIComponent(data)], {type: type}); - } - } - callback(blob); - }; - - if (canvas_proto.toDataURLHD) { - canvas_proto.toBlobHD = function() { - to_data_url = "toDataURLHD"; - var blob = this.toBlob(); - to_data_url = "toDataURL"; - return blob; - } - } else { - canvas_proto.toBlobHD = canvas_proto.toBlob; - } -} -}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); diff --git a/vendor/file-saver/FileSaver.min.js b/vendor/file-saver/FileSaver.min.js deleted file mode 100644 index eb062f2..0000000 --- a/vendor/file-saver/FileSaver.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ -var saveAs=saveAs||function(view){"use strict";if(typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var doc=view.document,get_URL=function(){return view.URL||view.webkitURL||view},save_link=doc.createElementNS("http://www.w3.org/1999/xhtml","a"),can_use_save_link="download"in save_link,click=function(node){var event=new MouseEvent("click");node.dispatchEvent(event)},is_safari=/Version\/[\d\.]+.*Safari/.test(navigator.userAgent),webkit_req_fs=view.webkitRequestFileSystem,req_fs=view.requestFileSystem||webkit_req_fs||view.mozRequestFileSystem,throw_outside=function(ex){(view.setImmediate||view.setTimeout)(function(){throw ex},0)},force_saveable_type="application/octet-stream",fs_min_size=0,arbitrary_revoke_timeout=500,revoke=function(file){var revoker=function(){if(typeof file==="string"){get_URL().revokeObjectURL(file)}else{file.remove()}};if(view.chrome){revoker()}else{setTimeout(revoker,arbitrary_revoke_timeout)}},dispatch=function(filesaver,event_types,event){event_types=[].concat(event_types);var i=event_types.length;while(i--){var listener=filesaver["on"+event_types[i]];if(typeof listener==="function"){try{listener.call(filesaver,event||filesaver)}catch(ex){throw_outside(ex)}}}},auto_bom=function(blob){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)){return new Blob(["\ufeff",blob],{type:blob.type})}return blob},FileSaver=function(blob,name,no_auto_bom){if(!no_auto_bom){blob=auto_bom(blob)}var filesaver=this,type=blob.type,blob_changed=false,object_url,target_view,dispatch_all=function(){dispatch(filesaver,"writestart progress write writeend".split(" "))},fs_error=function(){if(target_view&&is_safari&&typeof FileReader!=="undefined"){var reader=new FileReader;reader.onloadend=function(){var base64Data=reader.result;target_view.location.href="data:attachment/file"+base64Data.slice(base64Data.search(/[,;]/));filesaver.readyState=filesaver.DONE;dispatch_all()};reader.readAsDataURL(blob);filesaver.readyState=filesaver.INIT;return}if(blob_changed||!object_url){object_url=get_URL().createObjectURL(blob)}if(target_view){target_view.location.href=object_url}else{var new_tab=view.open(object_url,"_blank");if(new_tab==undefined&&is_safari){view.location.href=object_url}}filesaver.readyState=filesaver.DONE;dispatch_all();revoke(object_url)},abortable=function(func){return function(){if(filesaver.readyState!==filesaver.DONE){return func.apply(this,arguments)}}},create_if_not_found={create:true,exclusive:false},slice;filesaver.readyState=filesaver.INIT;if(!name){name="download"}if(can_use_save_link){object_url=get_URL().createObjectURL(blob);setTimeout(function(){save_link.href=object_url;save_link.download=name;click(save_link);dispatch_all();revoke(object_url);filesaver.readyState=filesaver.DONE});return}if(view.chrome&&type&&type!==force_saveable_type){slice=blob.slice||blob.webkitSlice;blob=slice.call(blob,0,blob.size,force_saveable_type);blob_changed=true}if(webkit_req_fs&&name!=="download"){name+=".download"}if(type===force_saveable_type||webkit_req_fs){target_view=view}if(!req_fs){fs_error();return}fs_min_size+=blob.size;req_fs(view.TEMPORARY,fs_min_size,abortable(function(fs){fs.root.getDirectory("saved",create_if_not_found,abortable(function(dir){var save=function(){dir.getFile(name,create_if_not_found,abortable(function(file){file.createWriter(abortable(function(writer){writer.onwriteend=function(event){target_view.location.href=file.toURL();filesaver.readyState=filesaver.DONE;dispatch(filesaver,"writeend",event);revoke(file)};writer.onerror=function(){var error=writer.error;if(error.code!==error.ABORT_ERR){fs_error()}};"writestart progress write abort".split(" ").forEach(function(event){writer["on"+event]=filesaver["on"+event]});writer.write(blob);filesaver.abort=function(){writer.abort();filesaver.readyState=filesaver.DONE};filesaver.readyState=filesaver.WRITING}),fs_error)}),fs_error)};dir.getFile(name,{create:false},abortable(function(file){file.remove();save()}),abortable(function(ex){if(ex.code===ex.NOT_FOUND_ERR){save()}else{fs_error()}}))}),fs_error)}),fs_error)},FS_proto=FileSaver.prototype,saveAs=function(blob,name,no_auto_bom){return new FileSaver(blob,name,no_auto_bom)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(blob,name,no_auto_bom){if(!no_auto_bom){blob=auto_bom(blob)}return navigator.msSaveOrOpenBlob(blob,name||"download")}}FS_proto.abort=function(){var filesaver=this;filesaver.readyState=filesaver.DONE;dispatch(filesaver,"abort")};FS_proto.readyState=FS_proto.INIT=0;FS_proto.WRITING=1;FS_proto.DONE=2;FS_proto.error=FS_proto.onwritestart=FS_proto.onprogress=FS_proto.onwrite=FS_proto.onabort=FS_proto.onerror=FS_proto.onwriteend=null;return saveAs}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!=null){define([],function(){return saveAs})} diff --git a/vendor/fontawesome/css/fa-brands.css b/vendor/fontawesome/css/fa-brands.css deleted file mode 100644 index 3e94b08..0000000 --- a/vendor/fontawesome/css/fa-brands.css +++ /dev/null @@ -1,13 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face { - font-family: 'Font Awesome 5 Brands'; - font-style: normal; - font-weight: normal; - src: url("../webfonts/fa-brands-400.eot"); - src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } - -.fab { - font-family: 'Font Awesome 5 Brands'; } diff --git a/vendor/fontawesome/css/fa-brands.min.css b/vendor/fontawesome/css/fa-brands.min.css deleted file mode 100644 index 15046ce..0000000 --- a/vendor/fontawesome/css/fa-brands.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands} \ No newline at end of file diff --git a/vendor/fontawesome/css/fa-regular.css b/vendor/fontawesome/css/fa-regular.css deleted file mode 100644 index a78295a..0000000 --- a/vendor/fontawesome/css/fa-regular.css +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face { - font-family: 'Font Awesome 5 Free'; - font-style: normal; - font-weight: 400; - src: url("../webfonts/fa-regular-400.eot"); - src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } - -.far { - font-family: 'Font Awesome 5 Free'; - font-weight: 400; } diff --git a/vendor/fontawesome/css/fa-regular.min.css b/vendor/fontawesome/css/fa-regular.min.css deleted file mode 100644 index ccdec70..0000000 --- a/vendor/fontawesome/css/fa-regular.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} \ No newline at end of file diff --git a/vendor/fontawesome/css/fa-solid.css b/vendor/fontawesome/css/fa-solid.css deleted file mode 100644 index 81f51cc..0000000 --- a/vendor/fontawesome/css/fa-solid.css +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face { - font-family: 'Font Awesome 5 Free'; - font-style: normal; - font-weight: 900; - src: url("../webfonts/fa-solid-900.eot"); - src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } - -.fa, -.fas { - font-family: 'Font Awesome 5 Free'; - font-weight: 900; } diff --git a/vendor/fontawesome/css/fa-solid.min.css b/vendor/fontawesome/css/fa-solid.min.css deleted file mode 100644 index b718a31..0000000 --- a/vendor/fontawesome/css/fa-solid.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} \ No newline at end of file diff --git a/vendor/fontawesome/css/fontawesome-all.css b/vendor/fontawesome/css/fontawesome-all.css deleted file mode 100644 index e32cbcd..0000000 --- a/vendor/fontawesome/css/fontawesome-all.css +++ /dev/null @@ -1,2726 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa, -.fas, -.far, -.fal, -.fab { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-block; - font-style: normal; - font-variant: normal; - text-rendering: auto; - line-height: 1; } - -.fa-lg { - font-size: 1.33333em; - line-height: 0.75em; - vertical-align: -.0667em; } - -.fa-xs { - font-size: .75em; } - -.fa-sm { - font-size: .875em; } - -.fa-1x { - font-size: 1em; } - -.fa-2x { - font-size: 2em; } - -.fa-3x { - font-size: 3em; } - -.fa-4x { - font-size: 4em; } - -.fa-5x { - font-size: 5em; } - -.fa-6x { - font-size: 6em; } - -.fa-7x { - font-size: 7em; } - -.fa-8x { - font-size: 8em; } - -.fa-9x { - font-size: 9em; } - -.fa-10x { - font-size: 10em; } - -.fa-fw { - text-align: center; - width: 1.25em; } - -.fa-ul { - list-style-type: none; - margin-left: 2.5em; - padding-left: 0; } - .fa-ul > li { - position: relative; } - -.fa-li { - left: -2em; - position: absolute; - text-align: center; - width: 2em; - line-height: inherit; } - -.fa-border { - border: solid 0.08em #eee; - border-radius: .1em; - padding: .2em .25em .15em; } - -.fa-pull-left { - float: left; } - -.fa-pull-right { - float: right; } - -.fa.fa-pull-left, -.fas.fa-pull-left, -.far.fa-pull-left, -.fal.fa-pull-left, -.fab.fa-pull-left { - margin-right: .3em; } - -.fa.fa-pull-right, -.fas.fa-pull-right, -.far.fa-pull-right, -.fal.fa-pull-right, -.fab.fa-pull-right { - margin-left: .3em; } - -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; } - -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); } - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - -.fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); } - -.fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - transform: rotate(180deg); } - -.fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - transform: rotate(270deg); } - -.fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); } - -.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - transform: scale(1, -1); } - -.fa-flip-horizontal.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(-1, -1); - transform: scale(-1, -1); } - -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - -webkit-filter: none; - filter: none; } - -.fa-stack { - display: inline-block; - height: 2em; - line-height: 2em; - position: relative; - vertical-align: middle; - width: 2em; } - -.fa-stack-1x, -.fa-stack-2x { - left: 0; - position: absolute; - text-align: center; - width: 100%; } - -.fa-stack-1x { - line-height: inherit; } - -.fa-stack-2x { - font-size: 2em; } - -.fa-inverse { - color: #fff; } - -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen -readers do not read off random characters that represent icons */ -.fa-500px:before { - content: "\f26e"; } - -.fa-accessible-icon:before { - content: "\f368"; } - -.fa-accusoft:before { - content: "\f369"; } - -.fa-address-book:before { - content: "\f2b9"; } - -.fa-address-card:before { - content: "\f2bb"; } - -.fa-adjust:before { - content: "\f042"; } - -.fa-adn:before { - content: "\f170"; } - -.fa-adversal:before { - content: "\f36a"; } - -.fa-affiliatetheme:before { - content: "\f36b"; } - -.fa-algolia:before { - content: "\f36c"; } - -.fa-align-center:before { - content: "\f037"; } - -.fa-align-justify:before { - content: "\f039"; } - -.fa-align-left:before { - content: "\f036"; } - -.fa-align-right:before { - content: "\f038"; } - -.fa-amazon:before { - content: "\f270"; } - -.fa-amazon-pay:before { - content: "\f42c"; } - -.fa-ambulance:before { - content: "\f0f9"; } - -.fa-american-sign-language-interpreting:before { - content: "\f2a3"; } - -.fa-amilia:before { - content: "\f36d"; } - -.fa-anchor:before { - content: "\f13d"; } - -.fa-android:before { - content: "\f17b"; } - -.fa-angellist:before { - content: "\f209"; } - -.fa-angle-double-down:before { - content: "\f103"; } - -.fa-angle-double-left:before { - content: "\f100"; } - -.fa-angle-double-right:before { - content: "\f101"; } - -.fa-angle-double-up:before { - content: "\f102"; } - -.fa-angle-down:before { - content: "\f107"; } - -.fa-angle-left:before { - content: "\f104"; } - -.fa-angle-right:before { - content: "\f105"; } - -.fa-angle-up:before { - content: "\f106"; } - -.fa-angrycreative:before { - content: "\f36e"; } - -.fa-angular:before { - content: "\f420"; } - -.fa-app-store:before { - content: "\f36f"; } - -.fa-app-store-ios:before { - content: "\f370"; } - -.fa-apper:before { - content: "\f371"; } - -.fa-apple:before { - content: "\f179"; } - -.fa-apple-pay:before { - content: "\f415"; } - -.fa-archive:before { - content: "\f187"; } - -.fa-arrow-alt-circle-down:before { - content: "\f358"; } - -.fa-arrow-alt-circle-left:before { - content: "\f359"; } - -.fa-arrow-alt-circle-right:before { - content: "\f35a"; } - -.fa-arrow-alt-circle-up:before { - content: "\f35b"; } - -.fa-arrow-circle-down:before { - content: "\f0ab"; } - -.fa-arrow-circle-left:before { - content: "\f0a8"; } - -.fa-arrow-circle-right:before { - content: "\f0a9"; } - -.fa-arrow-circle-up:before { - content: "\f0aa"; } - -.fa-arrow-down:before { - content: "\f063"; } - -.fa-arrow-left:before { - content: "\f060"; } - -.fa-arrow-right:before { - content: "\f061"; } - -.fa-arrow-up:before { - content: "\f062"; } - -.fa-arrows-alt:before { - content: "\f0b2"; } - -.fa-arrows-alt-h:before { - content: "\f337"; } - -.fa-arrows-alt-v:before { - content: "\f338"; } - -.fa-assistive-listening-systems:before { - content: "\f2a2"; } - -.fa-asterisk:before { - content: "\f069"; } - -.fa-asymmetrik:before { - content: "\f372"; } - -.fa-at:before { - content: "\f1fa"; } - -.fa-audible:before { - content: "\f373"; } - -.fa-audio-description:before { - content: "\f29e"; } - -.fa-autoprefixer:before { - content: "\f41c"; } - -.fa-avianex:before { - content: "\f374"; } - -.fa-aviato:before { - content: "\f421"; } - -.fa-aws:before { - content: "\f375"; } - -.fa-backward:before { - content: "\f04a"; } - -.fa-balance-scale:before { - content: "\f24e"; } - -.fa-ban:before { - content: "\f05e"; } - -.fa-band-aid:before { - content: "\f462"; } - -.fa-bandcamp:before { - content: "\f2d5"; } - -.fa-barcode:before { - content: "\f02a"; } - -.fa-bars:before { - content: "\f0c9"; } - -.fa-baseball-ball:before { - content: "\f433"; } - -.fa-basketball-ball:before { - content: "\f434"; } - -.fa-bath:before { - content: "\f2cd"; } - -.fa-battery-empty:before { - content: "\f244"; } - -.fa-battery-full:before { - content: "\f240"; } - -.fa-battery-half:before { - content: "\f242"; } - -.fa-battery-quarter:before { - content: "\f243"; } - -.fa-battery-three-quarters:before { - content: "\f241"; } - -.fa-bed:before { - content: "\f236"; } - -.fa-beer:before { - content: "\f0fc"; } - -.fa-behance:before { - content: "\f1b4"; } - -.fa-behance-square:before { - content: "\f1b5"; } - -.fa-bell:before { - content: "\f0f3"; } - -.fa-bell-slash:before { - content: "\f1f6"; } - -.fa-bicycle:before { - content: "\f206"; } - -.fa-bimobject:before { - content: "\f378"; } - -.fa-binoculars:before { - content: "\f1e5"; } - -.fa-birthday-cake:before { - content: "\f1fd"; } - -.fa-bitbucket:before { - content: "\f171"; } - -.fa-bitcoin:before { - content: "\f379"; } - -.fa-bity:before { - content: "\f37a"; } - -.fa-black-tie:before { - content: "\f27e"; } - -.fa-blackberry:before { - content: "\f37b"; } - -.fa-blind:before { - content: "\f29d"; } - -.fa-blogger:before { - content: "\f37c"; } - -.fa-blogger-b:before { - content: "\f37d"; } - -.fa-bluetooth:before { - content: "\f293"; } - -.fa-bluetooth-b:before { - content: "\f294"; } - -.fa-bold:before { - content: "\f032"; } - -.fa-bolt:before { - content: "\f0e7"; } - -.fa-bomb:before { - content: "\f1e2"; } - -.fa-book:before { - content: "\f02d"; } - -.fa-bookmark:before { - content: "\f02e"; } - -.fa-bowling-ball:before { - content: "\f436"; } - -.fa-box:before { - content: "\f466"; } - -.fa-boxes:before { - content: "\f468"; } - -.fa-braille:before { - content: "\f2a1"; } - -.fa-briefcase:before { - content: "\f0b1"; } - -.fa-btc:before { - content: "\f15a"; } - -.fa-bug:before { - content: "\f188"; } - -.fa-building:before { - content: "\f1ad"; } - -.fa-bullhorn:before { - content: "\f0a1"; } - -.fa-bullseye:before { - content: "\f140"; } - -.fa-buromobelexperte:before { - content: "\f37f"; } - -.fa-bus:before { - content: "\f207"; } - -.fa-buysellads:before { - content: "\f20d"; } - -.fa-calculator:before { - content: "\f1ec"; } - -.fa-calendar:before { - content: "\f133"; } - -.fa-calendar-alt:before { - content: "\f073"; } - -.fa-calendar-check:before { - content: "\f274"; } - -.fa-calendar-minus:before { - content: "\f272"; } - -.fa-calendar-plus:before { - content: "\f271"; } - -.fa-calendar-times:before { - content: "\f273"; } - -.fa-camera:before { - content: "\f030"; } - -.fa-camera-retro:before { - content: "\f083"; } - -.fa-car:before { - content: "\f1b9"; } - -.fa-caret-down:before { - content: "\f0d7"; } - -.fa-caret-left:before { - content: "\f0d9"; } - -.fa-caret-right:before { - content: "\f0da"; } - -.fa-caret-square-down:before { - content: "\f150"; } - -.fa-caret-square-left:before { - content: "\f191"; } - -.fa-caret-square-right:before { - content: "\f152"; } - -.fa-caret-square-up:before { - content: "\f151"; } - -.fa-caret-up:before { - content: "\f0d8"; } - -.fa-cart-arrow-down:before { - content: "\f218"; } - -.fa-cart-plus:before { - content: "\f217"; } - -.fa-cc-amazon-pay:before { - content: "\f42d"; } - -.fa-cc-amex:before { - content: "\f1f3"; } - -.fa-cc-apple-pay:before { - content: "\f416"; } - -.fa-cc-diners-club:before { - content: "\f24c"; } - -.fa-cc-discover:before { - content: "\f1f2"; } - -.fa-cc-jcb:before { - content: "\f24b"; } - -.fa-cc-mastercard:before { - content: "\f1f1"; } - -.fa-cc-paypal:before { - content: "\f1f4"; } - -.fa-cc-stripe:before { - content: "\f1f5"; } - -.fa-cc-visa:before { - content: "\f1f0"; } - -.fa-centercode:before { - content: "\f380"; } - -.fa-certificate:before { - content: "\f0a3"; } - -.fa-chart-area:before { - content: "\f1fe"; } - -.fa-chart-bar:before { - content: "\f080"; } - -.fa-chart-line:before { - content: "\f201"; } - -.fa-chart-pie:before { - content: "\f200"; } - -.fa-check:before { - content: "\f00c"; } - -.fa-check-circle:before { - content: "\f058"; } - -.fa-check-square:before { - content: "\f14a"; } - -.fa-chess:before { - content: "\f439"; } - -.fa-chess-bishop:before { - content: "\f43a"; } - -.fa-chess-board:before { - content: "\f43c"; } - -.fa-chess-king:before { - content: "\f43f"; } - -.fa-chess-knight:before { - content: "\f441"; } - -.fa-chess-pawn:before { - content: "\f443"; } - -.fa-chess-queen:before { - content: "\f445"; } - -.fa-chess-rook:before { - content: "\f447"; } - -.fa-chevron-circle-down:before { - content: "\f13a"; } - -.fa-chevron-circle-left:before { - content: "\f137"; } - -.fa-chevron-circle-right:before { - content: "\f138"; } - -.fa-chevron-circle-up:before { - content: "\f139"; } - -.fa-chevron-down:before { - content: "\f078"; } - -.fa-chevron-left:before { - content: "\f053"; } - -.fa-chevron-right:before { - content: "\f054"; } - -.fa-chevron-up:before { - content: "\f077"; } - -.fa-child:before { - content: "\f1ae"; } - -.fa-chrome:before { - content: "\f268"; } - -.fa-circle:before { - content: "\f111"; } - -.fa-circle-notch:before { - content: "\f1ce"; } - -.fa-clipboard:before { - content: "\f328"; } - -.fa-clipboard-check:before { - content: "\f46c"; } - -.fa-clipboard-list:before { - content: "\f46d"; } - -.fa-clock:before { - content: "\f017"; } - -.fa-clone:before { - content: "\f24d"; } - -.fa-closed-captioning:before { - content: "\f20a"; } - -.fa-cloud:before { - content: "\f0c2"; } - -.fa-cloud-download-alt:before { - content: "\f381"; } - -.fa-cloud-upload-alt:before { - content: "\f382"; } - -.fa-cloudscale:before { - content: "\f383"; } - -.fa-cloudsmith:before { - content: "\f384"; } - -.fa-cloudversify:before { - content: "\f385"; } - -.fa-code:before { - content: "\f121"; } - -.fa-code-branch:before { - content: "\f126"; } - -.fa-codepen:before { - content: "\f1cb"; } - -.fa-codiepie:before { - content: "\f284"; } - -.fa-coffee:before { - content: "\f0f4"; } - -.fa-cog:before { - content: "\f013"; } - -.fa-cogs:before { - content: "\f085"; } - -.fa-columns:before { - content: "\f0db"; } - -.fa-comment:before { - content: "\f075"; } - -.fa-comment-alt:before { - content: "\f27a"; } - -.fa-comments:before { - content: "\f086"; } - -.fa-compass:before { - content: "\f14e"; } - -.fa-compress:before { - content: "\f066"; } - -.fa-connectdevelop:before { - content: "\f20e"; } - -.fa-contao:before { - content: "\f26d"; } - -.fa-copy:before { - content: "\f0c5"; } - -.fa-copyright:before { - content: "\f1f9"; } - -.fa-cpanel:before { - content: "\f388"; } - -.fa-creative-commons:before { - content: "\f25e"; } - -.fa-credit-card:before { - content: "\f09d"; } - -.fa-crop:before { - content: "\f125"; } - -.fa-crosshairs:before { - content: "\f05b"; } - -.fa-css3:before { - content: "\f13c"; } - -.fa-css3-alt:before { - content: "\f38b"; } - -.fa-cube:before { - content: "\f1b2"; } - -.fa-cubes:before { - content: "\f1b3"; } - -.fa-cut:before { - content: "\f0c4"; } - -.fa-cuttlefish:before { - content: "\f38c"; } - -.fa-d-and-d:before { - content: "\f38d"; } - -.fa-dashcube:before { - content: "\f210"; } - -.fa-database:before { - content: "\f1c0"; } - -.fa-deaf:before { - content: "\f2a4"; } - -.fa-delicious:before { - content: "\f1a5"; } - -.fa-deploydog:before { - content: "\f38e"; } - -.fa-deskpro:before { - content: "\f38f"; } - -.fa-desktop:before { - content: "\f108"; } - -.fa-deviantart:before { - content: "\f1bd"; } - -.fa-digg:before { - content: "\f1a6"; } - -.fa-digital-ocean:before { - content: "\f391"; } - -.fa-discord:before { - content: "\f392"; } - -.fa-discourse:before { - content: "\f393"; } - -.fa-dna:before { - content: "\f471"; } - -.fa-dochub:before { - content: "\f394"; } - -.fa-docker:before { - content: "\f395"; } - -.fa-dollar-sign:before { - content: "\f155"; } - -.fa-dolly:before { - content: "\f472"; } - -.fa-dolly-flatbed:before { - content: "\f474"; } - -.fa-dot-circle:before { - content: "\f192"; } - -.fa-download:before { - content: "\f019"; } - -.fa-draft2digital:before { - content: "\f396"; } - -.fa-dribbble:before { - content: "\f17d"; } - -.fa-dribbble-square:before { - content: "\f397"; } - -.fa-dropbox:before { - content: "\f16b"; } - -.fa-drupal:before { - content: "\f1a9"; } - -.fa-dyalog:before { - content: "\f399"; } - -.fa-earlybirds:before { - content: "\f39a"; } - -.fa-edge:before { - content: "\f282"; } - -.fa-edit:before { - content: "\f044"; } - -.fa-eject:before { - content: "\f052"; } - -.fa-elementor:before { - content: "\f430"; } - -.fa-ellipsis-h:before { - content: "\f141"; } - -.fa-ellipsis-v:before { - content: "\f142"; } - -.fa-ember:before { - content: "\f423"; } - -.fa-empire:before { - content: "\f1d1"; } - -.fa-envelope:before { - content: "\f0e0"; } - -.fa-envelope-open:before { - content: "\f2b6"; } - -.fa-envelope-square:before { - content: "\f199"; } - -.fa-envira:before { - content: "\f299"; } - -.fa-eraser:before { - content: "\f12d"; } - -.fa-erlang:before { - content: "\f39d"; } - -.fa-ethereum:before { - content: "\f42e"; } - -.fa-etsy:before { - content: "\f2d7"; } - -.fa-euro-sign:before { - content: "\f153"; } - -.fa-exchange-alt:before { - content: "\f362"; } - -.fa-exclamation:before { - content: "\f12a"; } - -.fa-exclamation-circle:before { - content: "\f06a"; } - -.fa-exclamation-triangle:before { - content: "\f071"; } - -.fa-expand:before { - content: "\f065"; } - -.fa-expand-arrows-alt:before { - content: "\f31e"; } - -.fa-expeditedssl:before { - content: "\f23e"; } - -.fa-external-link-alt:before { - content: "\f35d"; } - -.fa-external-link-square-alt:before { - content: "\f360"; } - -.fa-eye:before { - content: "\f06e"; } - -.fa-eye-dropper:before { - content: "\f1fb"; } - -.fa-eye-slash:before { - content: "\f070"; } - -.fa-facebook:before { - content: "\f09a"; } - -.fa-facebook-f:before { - content: "\f39e"; } - -.fa-facebook-messenger:before { - content: "\f39f"; } - -.fa-facebook-square:before { - content: "\f082"; } - -.fa-fast-backward:before { - content: "\f049"; } - -.fa-fast-forward:before { - content: "\f050"; } - -.fa-fax:before { - content: "\f1ac"; } - -.fa-female:before { - content: "\f182"; } - -.fa-fighter-jet:before { - content: "\f0fb"; } - -.fa-file:before { - content: "\f15b"; } - -.fa-file-alt:before { - content: "\f15c"; } - -.fa-file-archive:before { - content: "\f1c6"; } - -.fa-file-audio:before { - content: "\f1c7"; } - -.fa-file-code:before { - content: "\f1c9"; } - -.fa-file-excel:before { - content: "\f1c3"; } - -.fa-file-image:before { - content: "\f1c5"; } - -.fa-file-pdf:before { - content: "\f1c1"; } - -.fa-file-powerpoint:before { - content: "\f1c4"; } - -.fa-file-video:before { - content: "\f1c8"; } - -.fa-file-word:before { - content: "\f1c2"; } - -.fa-film:before { - content: "\f008"; } - -.fa-filter:before { - content: "\f0b0"; } - -.fa-fire:before { - content: "\f06d"; } - -.fa-fire-extinguisher:before { - content: "\f134"; } - -.fa-firefox:before { - content: "\f269"; } - -.fa-first-aid:before { - content: "\f479"; } - -.fa-first-order:before { - content: "\f2b0"; } - -.fa-firstdraft:before { - content: "\f3a1"; } - -.fa-flag:before { - content: "\f024"; } - -.fa-flag-checkered:before { - content: "\f11e"; } - -.fa-flask:before { - content: "\f0c3"; } - -.fa-flickr:before { - content: "\f16e"; } - -.fa-flipboard:before { - content: "\f44d"; } - -.fa-fly:before { - content: "\f417"; } - -.fa-folder:before { - content: "\f07b"; } - -.fa-folder-open:before { - content: "\f07c"; } - -.fa-font:before { - content: "\f031"; } - -.fa-font-awesome:before { - content: "\f2b4"; } - -.fa-font-awesome-alt:before { - content: "\f35c"; } - -.fa-font-awesome-flag:before { - content: "\f425"; } - -.fa-fonticons:before { - content: "\f280"; } - -.fa-fonticons-fi:before { - content: "\f3a2"; } - -.fa-football-ball:before { - content: "\f44e"; } - -.fa-fort-awesome:before { - content: "\f286"; } - -.fa-fort-awesome-alt:before { - content: "\f3a3"; } - -.fa-forumbee:before { - content: "\f211"; } - -.fa-forward:before { - content: "\f04e"; } - -.fa-foursquare:before { - content: "\f180"; } - -.fa-free-code-camp:before { - content: "\f2c5"; } - -.fa-freebsd:before { - content: "\f3a4"; } - -.fa-frown:before { - content: "\f119"; } - -.fa-futbol:before { - content: "\f1e3"; } - -.fa-gamepad:before { - content: "\f11b"; } - -.fa-gavel:before { - content: "\f0e3"; } - -.fa-gem:before { - content: "\f3a5"; } - -.fa-genderless:before { - content: "\f22d"; } - -.fa-get-pocket:before { - content: "\f265"; } - -.fa-gg:before { - content: "\f260"; } - -.fa-gg-circle:before { - content: "\f261"; } - -.fa-gift:before { - content: "\f06b"; } - -.fa-git:before { - content: "\f1d3"; } - -.fa-git-square:before { - content: "\f1d2"; } - -.fa-github:before { - content: "\f09b"; } - -.fa-github-alt:before { - content: "\f113"; } - -.fa-github-square:before { - content: "\f092"; } - -.fa-gitkraken:before { - content: "\f3a6"; } - -.fa-gitlab:before { - content: "\f296"; } - -.fa-gitter:before { - content: "\f426"; } - -.fa-glass-martini:before { - content: "\f000"; } - -.fa-glide:before { - content: "\f2a5"; } - -.fa-glide-g:before { - content: "\f2a6"; } - -.fa-globe:before { - content: "\f0ac"; } - -.fa-gofore:before { - content: "\f3a7"; } - -.fa-golf-ball:before { - content: "\f450"; } - -.fa-goodreads:before { - content: "\f3a8"; } - -.fa-goodreads-g:before { - content: "\f3a9"; } - -.fa-google:before { - content: "\f1a0"; } - -.fa-google-drive:before { - content: "\f3aa"; } - -.fa-google-play:before { - content: "\f3ab"; } - -.fa-google-plus:before { - content: "\f2b3"; } - -.fa-google-plus-g:before { - content: "\f0d5"; } - -.fa-google-plus-square:before { - content: "\f0d4"; } - -.fa-google-wallet:before { - content: "\f1ee"; } - -.fa-graduation-cap:before { - content: "\f19d"; } - -.fa-gratipay:before { - content: "\f184"; } - -.fa-grav:before { - content: "\f2d6"; } - -.fa-gripfire:before { - content: "\f3ac"; } - -.fa-grunt:before { - content: "\f3ad"; } - -.fa-gulp:before { - content: "\f3ae"; } - -.fa-h-square:before { - content: "\f0fd"; } - -.fa-hacker-news:before { - content: "\f1d4"; } - -.fa-hacker-news-square:before { - content: "\f3af"; } - -.fa-hand-lizard:before { - content: "\f258"; } - -.fa-hand-paper:before { - content: "\f256"; } - -.fa-hand-peace:before { - content: "\f25b"; } - -.fa-hand-point-down:before { - content: "\f0a7"; } - -.fa-hand-point-left:before { - content: "\f0a5"; } - -.fa-hand-point-right:before { - content: "\f0a4"; } - -.fa-hand-point-up:before { - content: "\f0a6"; } - -.fa-hand-pointer:before { - content: "\f25a"; } - -.fa-hand-rock:before { - content: "\f255"; } - -.fa-hand-scissors:before { - content: "\f257"; } - -.fa-hand-spock:before { - content: "\f259"; } - -.fa-handshake:before { - content: "\f2b5"; } - -.fa-hashtag:before { - content: "\f292"; } - -.fa-hdd:before { - content: "\f0a0"; } - -.fa-heading:before { - content: "\f1dc"; } - -.fa-headphones:before { - content: "\f025"; } - -.fa-heart:before { - content: "\f004"; } - -.fa-heartbeat:before { - content: "\f21e"; } - -.fa-hips:before { - content: "\f452"; } - -.fa-hire-a-helper:before { - content: "\f3b0"; } - -.fa-history:before { - content: "\f1da"; } - -.fa-hockey-puck:before { - content: "\f453"; } - -.fa-home:before { - content: "\f015"; } - -.fa-hooli:before { - content: "\f427"; } - -.fa-hospital:before { - content: "\f0f8"; } - -.fa-hospital-symbol:before { - content: "\f47e"; } - -.fa-hotjar:before { - content: "\f3b1"; } - -.fa-hourglass:before { - content: "\f254"; } - -.fa-hourglass-end:before { - content: "\f253"; } - -.fa-hourglass-half:before { - content: "\f252"; } - -.fa-hourglass-start:before { - content: "\f251"; } - -.fa-houzz:before { - content: "\f27c"; } - -.fa-html5:before { - content: "\f13b"; } - -.fa-hubspot:before { - content: "\f3b2"; } - -.fa-i-cursor:before { - content: "\f246"; } - -.fa-id-badge:before { - content: "\f2c1"; } - -.fa-id-card:before { - content: "\f2c2"; } - -.fa-image:before { - content: "\f03e"; } - -.fa-images:before { - content: "\f302"; } - -.fa-imdb:before { - content: "\f2d8"; } - -.fa-inbox:before { - content: "\f01c"; } - -.fa-indent:before { - content: "\f03c"; } - -.fa-industry:before { - content: "\f275"; } - -.fa-info:before { - content: "\f129"; } - -.fa-info-circle:before { - content: "\f05a"; } - -.fa-instagram:before { - content: "\f16d"; } - -.fa-internet-explorer:before { - content: "\f26b"; } - -.fa-ioxhost:before { - content: "\f208"; } - -.fa-italic:before { - content: "\f033"; } - -.fa-itunes:before { - content: "\f3b4"; } - -.fa-itunes-note:before { - content: "\f3b5"; } - -.fa-jenkins:before { - content: "\f3b6"; } - -.fa-joget:before { - content: "\f3b7"; } - -.fa-joomla:before { - content: "\f1aa"; } - -.fa-js:before { - content: "\f3b8"; } - -.fa-js-square:before { - content: "\f3b9"; } - -.fa-jsfiddle:before { - content: "\f1cc"; } - -.fa-key:before { - content: "\f084"; } - -.fa-keyboard:before { - content: "\f11c"; } - -.fa-keycdn:before { - content: "\f3ba"; } - -.fa-kickstarter:before { - content: "\f3bb"; } - -.fa-kickstarter-k:before { - content: "\f3bc"; } - -.fa-korvue:before { - content: "\f42f"; } - -.fa-language:before { - content: "\f1ab"; } - -.fa-laptop:before { - content: "\f109"; } - -.fa-laravel:before { - content: "\f3bd"; } - -.fa-lastfm:before { - content: "\f202"; } - -.fa-lastfm-square:before { - content: "\f203"; } - -.fa-leaf:before { - content: "\f06c"; } - -.fa-leanpub:before { - content: "\f212"; } - -.fa-lemon:before { - content: "\f094"; } - -.fa-less:before { - content: "\f41d"; } - -.fa-level-down-alt:before { - content: "\f3be"; } - -.fa-level-up-alt:before { - content: "\f3bf"; } - -.fa-life-ring:before { - content: "\f1cd"; } - -.fa-lightbulb:before { - content: "\f0eb"; } - -.fa-line:before { - content: "\f3c0"; } - -.fa-link:before { - content: "\f0c1"; } - -.fa-linkedin:before { - content: "\f08c"; } - -.fa-linkedin-in:before { - content: "\f0e1"; } - -.fa-linode:before { - content: "\f2b8"; } - -.fa-linux:before { - content: "\f17c"; } - -.fa-lira-sign:before { - content: "\f195"; } - -.fa-list:before { - content: "\f03a"; } - -.fa-list-alt:before { - content: "\f022"; } - -.fa-list-ol:before { - content: "\f0cb"; } - -.fa-list-ul:before { - content: "\f0ca"; } - -.fa-location-arrow:before { - content: "\f124"; } - -.fa-lock:before { - content: "\f023"; } - -.fa-lock-open:before { - content: "\f3c1"; } - -.fa-long-arrow-alt-down:before { - content: "\f309"; } - -.fa-long-arrow-alt-left:before { - content: "\f30a"; } - -.fa-long-arrow-alt-right:before { - content: "\f30b"; } - -.fa-long-arrow-alt-up:before { - content: "\f30c"; } - -.fa-low-vision:before { - content: "\f2a8"; } - -.fa-lyft:before { - content: "\f3c3"; } - -.fa-magento:before { - content: "\f3c4"; } - -.fa-magic:before { - content: "\f0d0"; } - -.fa-magnet:before { - content: "\f076"; } - -.fa-male:before { - content: "\f183"; } - -.fa-map:before { - content: "\f279"; } - -.fa-map-marker:before { - content: "\f041"; } - -.fa-map-marker-alt:before { - content: "\f3c5"; } - -.fa-map-pin:before { - content: "\f276"; } - -.fa-map-signs:before { - content: "\f277"; } - -.fa-mars:before { - content: "\f222"; } - -.fa-mars-double:before { - content: "\f227"; } - -.fa-mars-stroke:before { - content: "\f229"; } - -.fa-mars-stroke-h:before { - content: "\f22b"; } - -.fa-mars-stroke-v:before { - content: "\f22a"; } - -.fa-maxcdn:before { - content: "\f136"; } - -.fa-medapps:before { - content: "\f3c6"; } - -.fa-medium:before { - content: "\f23a"; } - -.fa-medium-m:before { - content: "\f3c7"; } - -.fa-medkit:before { - content: "\f0fa"; } - -.fa-medrt:before { - content: "\f3c8"; } - -.fa-meetup:before { - content: "\f2e0"; } - -.fa-meh:before { - content: "\f11a"; } - -.fa-mercury:before { - content: "\f223"; } - -.fa-microchip:before { - content: "\f2db"; } - -.fa-microphone:before { - content: "\f130"; } - -.fa-microphone-slash:before { - content: "\f131"; } - -.fa-microsoft:before { - content: "\f3ca"; } - -.fa-minus:before { - content: "\f068"; } - -.fa-minus-circle:before { - content: "\f056"; } - -.fa-minus-square:before { - content: "\f146"; } - -.fa-mix:before { - content: "\f3cb"; } - -.fa-mixcloud:before { - content: "\f289"; } - -.fa-mizuni:before { - content: "\f3cc"; } - -.fa-mobile:before { - content: "\f10b"; } - -.fa-mobile-alt:before { - content: "\f3cd"; } - -.fa-modx:before { - content: "\f285"; } - -.fa-monero:before { - content: "\f3d0"; } - -.fa-money-bill-alt:before { - content: "\f3d1"; } - -.fa-moon:before { - content: "\f186"; } - -.fa-motorcycle:before { - content: "\f21c"; } - -.fa-mouse-pointer:before { - content: "\f245"; } - -.fa-music:before { - content: "\f001"; } - -.fa-napster:before { - content: "\f3d2"; } - -.fa-neuter:before { - content: "\f22c"; } - -.fa-newspaper:before { - content: "\f1ea"; } - -.fa-nintendo-switch:before { - content: "\f418"; } - -.fa-node:before { - content: "\f419"; } - -.fa-node-js:before { - content: "\f3d3"; } - -.fa-npm:before { - content: "\f3d4"; } - -.fa-ns8:before { - content: "\f3d5"; } - -.fa-nutritionix:before { - content: "\f3d6"; } - -.fa-object-group:before { - content: "\f247"; } - -.fa-object-ungroup:before { - content: "\f248"; } - -.fa-odnoklassniki:before { - content: "\f263"; } - -.fa-odnoklassniki-square:before { - content: "\f264"; } - -.fa-opencart:before { - content: "\f23d"; } - -.fa-openid:before { - content: "\f19b"; } - -.fa-opera:before { - content: "\f26a"; } - -.fa-optin-monster:before { - content: "\f23c"; } - -.fa-osi:before { - content: "\f41a"; } - -.fa-outdent:before { - content: "\f03b"; } - -.fa-page4:before { - content: "\f3d7"; } - -.fa-pagelines:before { - content: "\f18c"; } - -.fa-paint-brush:before { - content: "\f1fc"; } - -.fa-palfed:before { - content: "\f3d8"; } - -.fa-pallet:before { - content: "\f482"; } - -.fa-paper-plane:before { - content: "\f1d8"; } - -.fa-paperclip:before { - content: "\f0c6"; } - -.fa-paragraph:before { - content: "\f1dd"; } - -.fa-paste:before { - content: "\f0ea"; } - -.fa-patreon:before { - content: "\f3d9"; } - -.fa-pause:before { - content: "\f04c"; } - -.fa-pause-circle:before { - content: "\f28b"; } - -.fa-paw:before { - content: "\f1b0"; } - -.fa-paypal:before { - content: "\f1ed"; } - -.fa-pen-square:before { - content: "\f14b"; } - -.fa-pencil-alt:before { - content: "\f303"; } - -.fa-percent:before { - content: "\f295"; } - -.fa-periscope:before { - content: "\f3da"; } - -.fa-phabricator:before { - content: "\f3db"; } - -.fa-phoenix-framework:before { - content: "\f3dc"; } - -.fa-phone:before { - content: "\f095"; } - -.fa-phone-square:before { - content: "\f098"; } - -.fa-phone-volume:before { - content: "\f2a0"; } - -.fa-php:before { - content: "\f457"; } - -.fa-pied-piper:before { - content: "\f2ae"; } - -.fa-pied-piper-alt:before { - content: "\f1a8"; } - -.fa-pied-piper-pp:before { - content: "\f1a7"; } - -.fa-pills:before { - content: "\f484"; } - -.fa-pinterest:before { - content: "\f0d2"; } - -.fa-pinterest-p:before { - content: "\f231"; } - -.fa-pinterest-square:before { - content: "\f0d3"; } - -.fa-plane:before { - content: "\f072"; } - -.fa-play:before { - content: "\f04b"; } - -.fa-play-circle:before { - content: "\f144"; } - -.fa-playstation:before { - content: "\f3df"; } - -.fa-plug:before { - content: "\f1e6"; } - -.fa-plus:before { - content: "\f067"; } - -.fa-plus-circle:before { - content: "\f055"; } - -.fa-plus-square:before { - content: "\f0fe"; } - -.fa-podcast:before { - content: "\f2ce"; } - -.fa-pound-sign:before { - content: "\f154"; } - -.fa-power-off:before { - content: "\f011"; } - -.fa-print:before { - content: "\f02f"; } - -.fa-product-hunt:before { - content: "\f288"; } - -.fa-pushed:before { - content: "\f3e1"; } - -.fa-puzzle-piece:before { - content: "\f12e"; } - -.fa-python:before { - content: "\f3e2"; } - -.fa-qq:before { - content: "\f1d6"; } - -.fa-qrcode:before { - content: "\f029"; } - -.fa-question:before { - content: "\f128"; } - -.fa-question-circle:before { - content: "\f059"; } - -.fa-quidditch:before { - content: "\f458"; } - -.fa-quinscape:before { - content: "\f459"; } - -.fa-quora:before { - content: "\f2c4"; } - -.fa-quote-left:before { - content: "\f10d"; } - -.fa-quote-right:before { - content: "\f10e"; } - -.fa-random:before { - content: "\f074"; } - -.fa-ravelry:before { - content: "\f2d9"; } - -.fa-react:before { - content: "\f41b"; } - -.fa-rebel:before { - content: "\f1d0"; } - -.fa-recycle:before { - content: "\f1b8"; } - -.fa-red-river:before { - content: "\f3e3"; } - -.fa-reddit:before { - content: "\f1a1"; } - -.fa-reddit-alien:before { - content: "\f281"; } - -.fa-reddit-square:before { - content: "\f1a2"; } - -.fa-redo:before { - content: "\f01e"; } - -.fa-redo-alt:before { - content: "\f2f9"; } - -.fa-registered:before { - content: "\f25d"; } - -.fa-rendact:before { - content: "\f3e4"; } - -.fa-renren:before { - content: "\f18b"; } - -.fa-reply:before { - content: "\f3e5"; } - -.fa-reply-all:before { - content: "\f122"; } - -.fa-replyd:before { - content: "\f3e6"; } - -.fa-resolving:before { - content: "\f3e7"; } - -.fa-retweet:before { - content: "\f079"; } - -.fa-road:before { - content: "\f018"; } - -.fa-rocket:before { - content: "\f135"; } - -.fa-rocketchat:before { - content: "\f3e8"; } - -.fa-rockrms:before { - content: "\f3e9"; } - -.fa-rss:before { - content: "\f09e"; } - -.fa-rss-square:before { - content: "\f143"; } - -.fa-ruble-sign:before { - content: "\f158"; } - -.fa-rupee-sign:before { - content: "\f156"; } - -.fa-safari:before { - content: "\f267"; } - -.fa-sass:before { - content: "\f41e"; } - -.fa-save:before { - content: "\f0c7"; } - -.fa-schlix:before { - content: "\f3ea"; } - -.fa-scribd:before { - content: "\f28a"; } - -.fa-search:before { - content: "\f002"; } - -.fa-search-minus:before { - content: "\f010"; } - -.fa-search-plus:before { - content: "\f00e"; } - -.fa-searchengin:before { - content: "\f3eb"; } - -.fa-sellcast:before { - content: "\f2da"; } - -.fa-sellsy:before { - content: "\f213"; } - -.fa-server:before { - content: "\f233"; } - -.fa-servicestack:before { - content: "\f3ec"; } - -.fa-share:before { - content: "\f064"; } - -.fa-share-alt:before { - content: "\f1e0"; } - -.fa-share-alt-square:before { - content: "\f1e1"; } - -.fa-share-square:before { - content: "\f14d"; } - -.fa-shekel-sign:before { - content: "\f20b"; } - -.fa-shield-alt:before { - content: "\f3ed"; } - -.fa-ship:before { - content: "\f21a"; } - -.fa-shipping-fast:before { - content: "\f48b"; } - -.fa-shirtsinbulk:before { - content: "\f214"; } - -.fa-shopping-bag:before { - content: "\f290"; } - -.fa-shopping-basket:before { - content: "\f291"; } - -.fa-shopping-cart:before { - content: "\f07a"; } - -.fa-shower:before { - content: "\f2cc"; } - -.fa-sign-in-alt:before { - content: "\f2f6"; } - -.fa-sign-language:before { - content: "\f2a7"; } - -.fa-sign-out-alt:before { - content: "\f2f5"; } - -.fa-signal:before { - content: "\f012"; } - -.fa-simplybuilt:before { - content: "\f215"; } - -.fa-sistrix:before { - content: "\f3ee"; } - -.fa-sitemap:before { - content: "\f0e8"; } - -.fa-skyatlas:before { - content: "\f216"; } - -.fa-skype:before { - content: "\f17e"; } - -.fa-slack:before { - content: "\f198"; } - -.fa-slack-hash:before { - content: "\f3ef"; } - -.fa-sliders-h:before { - content: "\f1de"; } - -.fa-slideshare:before { - content: "\f1e7"; } - -.fa-smile:before { - content: "\f118"; } - -.fa-snapchat:before { - content: "\f2ab"; } - -.fa-snapchat-ghost:before { - content: "\f2ac"; } - -.fa-snapchat-square:before { - content: "\f2ad"; } - -.fa-snowflake:before { - content: "\f2dc"; } - -.fa-sort:before { - content: "\f0dc"; } - -.fa-sort-alpha-down:before { - content: "\f15d"; } - -.fa-sort-alpha-up:before { - content: "\f15e"; } - -.fa-sort-amount-down:before { - content: "\f160"; } - -.fa-sort-amount-up:before { - content: "\f161"; } - -.fa-sort-down:before { - content: "\f0dd"; } - -.fa-sort-numeric-down:before { - content: "\f162"; } - -.fa-sort-numeric-up:before { - content: "\f163"; } - -.fa-sort-up:before { - content: "\f0de"; } - -.fa-soundcloud:before { - content: "\f1be"; } - -.fa-space-shuttle:before { - content: "\f197"; } - -.fa-speakap:before { - content: "\f3f3"; } - -.fa-spinner:before { - content: "\f110"; } - -.fa-spotify:before { - content: "\f1bc"; } - -.fa-square:before { - content: "\f0c8"; } - -.fa-square-full:before { - content: "\f45c"; } - -.fa-stack-exchange:before { - content: "\f18d"; } - -.fa-stack-overflow:before { - content: "\f16c"; } - -.fa-star:before { - content: "\f005"; } - -.fa-star-half:before { - content: "\f089"; } - -.fa-staylinked:before { - content: "\f3f5"; } - -.fa-steam:before { - content: "\f1b6"; } - -.fa-steam-square:before { - content: "\f1b7"; } - -.fa-steam-symbol:before { - content: "\f3f6"; } - -.fa-step-backward:before { - content: "\f048"; } - -.fa-step-forward:before { - content: "\f051"; } - -.fa-stethoscope:before { - content: "\f0f1"; } - -.fa-sticker-mule:before { - content: "\f3f7"; } - -.fa-sticky-note:before { - content: "\f249"; } - -.fa-stop:before { - content: "\f04d"; } - -.fa-stop-circle:before { - content: "\f28d"; } - -.fa-stopwatch:before { - content: "\f2f2"; } - -.fa-strava:before { - content: "\f428"; } - -.fa-street-view:before { - content: "\f21d"; } - -.fa-strikethrough:before { - content: "\f0cc"; } - -.fa-stripe:before { - content: "\f429"; } - -.fa-stripe-s:before { - content: "\f42a"; } - -.fa-studiovinari:before { - content: "\f3f8"; } - -.fa-stumbleupon:before { - content: "\f1a4"; } - -.fa-stumbleupon-circle:before { - content: "\f1a3"; } - -.fa-subscript:before { - content: "\f12c"; } - -.fa-subway:before { - content: "\f239"; } - -.fa-suitcase:before { - content: "\f0f2"; } - -.fa-sun:before { - content: "\f185"; } - -.fa-superpowers:before { - content: "\f2dd"; } - -.fa-superscript:before { - content: "\f12b"; } - -.fa-supple:before { - content: "\f3f9"; } - -.fa-sync:before { - content: "\f021"; } - -.fa-sync-alt:before { - content: "\f2f1"; } - -.fa-syringe:before { - content: "\f48e"; } - -.fa-table:before { - content: "\f0ce"; } - -.fa-table-tennis:before { - content: "\f45d"; } - -.fa-tablet:before { - content: "\f10a"; } - -.fa-tablet-alt:before { - content: "\f3fa"; } - -.fa-tachometer-alt:before { - content: "\f3fd"; } - -.fa-tag:before { - content: "\f02b"; } - -.fa-tags:before { - content: "\f02c"; } - -.fa-tasks:before { - content: "\f0ae"; } - -.fa-taxi:before { - content: "\f1ba"; } - -.fa-telegram:before { - content: "\f2c6"; } - -.fa-telegram-plane:before { - content: "\f3fe"; } - -.fa-tencent-weibo:before { - content: "\f1d5"; } - -.fa-terminal:before { - content: "\f120"; } - -.fa-text-height:before { - content: "\f034"; } - -.fa-text-width:before { - content: "\f035"; } - -.fa-th:before { - content: "\f00a"; } - -.fa-th-large:before { - content: "\f009"; } - -.fa-th-list:before { - content: "\f00b"; } - -.fa-themeisle:before { - content: "\f2b2"; } - -.fa-thermometer:before { - content: "\f491"; } - -.fa-thermometer-empty:before { - content: "\f2cb"; } - -.fa-thermometer-full:before { - content: "\f2c7"; } - -.fa-thermometer-half:before { - content: "\f2c9"; } - -.fa-thermometer-quarter:before { - content: "\f2ca"; } - -.fa-thermometer-three-quarters:before { - content: "\f2c8"; } - -.fa-thumbs-down:before { - content: "\f165"; } - -.fa-thumbs-up:before { - content: "\f164"; } - -.fa-thumbtack:before { - content: "\f08d"; } - -.fa-ticket-alt:before { - content: "\f3ff"; } - -.fa-times:before { - content: "\f00d"; } - -.fa-times-circle:before { - content: "\f057"; } - -.fa-tint:before { - content: "\f043"; } - -.fa-toggle-off:before { - content: "\f204"; } - -.fa-toggle-on:before { - content: "\f205"; } - -.fa-trademark:before { - content: "\f25c"; } - -.fa-train:before { - content: "\f238"; } - -.fa-transgender:before { - content: "\f224"; } - -.fa-transgender-alt:before { - content: "\f225"; } - -.fa-trash:before { - content: "\f1f8"; } - -.fa-trash-alt:before { - content: "\f2ed"; } - -.fa-tree:before { - content: "\f1bb"; } - -.fa-trello:before { - content: "\f181"; } - -.fa-tripadvisor:before { - content: "\f262"; } - -.fa-trophy:before { - content: "\f091"; } - -.fa-truck:before { - content: "\f0d1"; } - -.fa-tty:before { - content: "\f1e4"; } - -.fa-tumblr:before { - content: "\f173"; } - -.fa-tumblr-square:before { - content: "\f174"; } - -.fa-tv:before { - content: "\f26c"; } - -.fa-twitch:before { - content: "\f1e8"; } - -.fa-twitter:before { - content: "\f099"; } - -.fa-twitter-square:before { - content: "\f081"; } - -.fa-typo3:before { - content: "\f42b"; } - -.fa-uber:before { - content: "\f402"; } - -.fa-uikit:before { - content: "\f403"; } - -.fa-umbrella:before { - content: "\f0e9"; } - -.fa-underline:before { - content: "\f0cd"; } - -.fa-undo:before { - content: "\f0e2"; } - -.fa-undo-alt:before { - content: "\f2ea"; } - -.fa-uniregistry:before { - content: "\f404"; } - -.fa-universal-access:before { - content: "\f29a"; } - -.fa-university:before { - content: "\f19c"; } - -.fa-unlink:before { - content: "\f127"; } - -.fa-unlock:before { - content: "\f09c"; } - -.fa-unlock-alt:before { - content: "\f13e"; } - -.fa-untappd:before { - content: "\f405"; } - -.fa-upload:before { - content: "\f093"; } - -.fa-usb:before { - content: "\f287"; } - -.fa-user:before { - content: "\f007"; } - -.fa-user-circle:before { - content: "\f2bd"; } - -.fa-user-md:before { - content: "\f0f0"; } - -.fa-user-plus:before { - content: "\f234"; } - -.fa-user-secret:before { - content: "\f21b"; } - -.fa-user-times:before { - content: "\f235"; } - -.fa-users:before { - content: "\f0c0"; } - -.fa-ussunnah:before { - content: "\f407"; } - -.fa-utensil-spoon:before { - content: "\f2e5"; } - -.fa-utensils:before { - content: "\f2e7"; } - -.fa-vaadin:before { - content: "\f408"; } - -.fa-venus:before { - content: "\f221"; } - -.fa-venus-double:before { - content: "\f226"; } - -.fa-venus-mars:before { - content: "\f228"; } - -.fa-viacoin:before { - content: "\f237"; } - -.fa-viadeo:before { - content: "\f2a9"; } - -.fa-viadeo-square:before { - content: "\f2aa"; } - -.fa-viber:before { - content: "\f409"; } - -.fa-video:before { - content: "\f03d"; } - -.fa-vimeo:before { - content: "\f40a"; } - -.fa-vimeo-square:before { - content: "\f194"; } - -.fa-vimeo-v:before { - content: "\f27d"; } - -.fa-vine:before { - content: "\f1ca"; } - -.fa-vk:before { - content: "\f189"; } - -.fa-vnv:before { - content: "\f40b"; } - -.fa-volleyball-ball:before { - content: "\f45f"; } - -.fa-volume-down:before { - content: "\f027"; } - -.fa-volume-off:before { - content: "\f026"; } - -.fa-volume-up:before { - content: "\f028"; } - -.fa-vuejs:before { - content: "\f41f"; } - -.fa-warehouse:before { - content: "\f494"; } - -.fa-weibo:before { - content: "\f18a"; } - -.fa-weight:before { - content: "\f496"; } - -.fa-weixin:before { - content: "\f1d7"; } - -.fa-whatsapp:before { - content: "\f232"; } - -.fa-whatsapp-square:before { - content: "\f40c"; } - -.fa-wheelchair:before { - content: "\f193"; } - -.fa-whmcs:before { - content: "\f40d"; } - -.fa-wifi:before { - content: "\f1eb"; } - -.fa-wikipedia-w:before { - content: "\f266"; } - -.fa-window-close:before { - content: "\f410"; } - -.fa-window-maximize:before { - content: "\f2d0"; } - -.fa-window-minimize:before { - content: "\f2d1"; } - -.fa-window-restore:before { - content: "\f2d2"; } - -.fa-windows:before { - content: "\f17a"; } - -.fa-won-sign:before { - content: "\f159"; } - -.fa-wordpress:before { - content: "\f19a"; } - -.fa-wordpress-simple:before { - content: "\f411"; } - -.fa-wpbeginner:before { - content: "\f297"; } - -.fa-wpexplorer:before { - content: "\f2de"; } - -.fa-wpforms:before { - content: "\f298"; } - -.fa-wrench:before { - content: "\f0ad"; } - -.fa-xbox:before { - content: "\f412"; } - -.fa-xing:before { - content: "\f168"; } - -.fa-xing-square:before { - content: "\f169"; } - -.fa-y-combinator:before { - content: "\f23b"; } - -.fa-yahoo:before { - content: "\f19e"; } - -.fa-yandex:before { - content: "\f413"; } - -.fa-yandex-international:before { - content: "\f414"; } - -.fa-yelp:before { - content: "\f1e9"; } - -.fa-yen-sign:before { - content: "\f157"; } - -.fa-yoast:before { - content: "\f2b1"; } - -.fa-youtube:before { - content: "\f167"; } - -.fa-youtube-square:before { - content: "\f431"; } - -.sr-only { - border: 0; - clip: rect(0, 0, 0, 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; } - -.sr-only-focusable:active, .sr-only-focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - width: auto; } -@font-face { - font-family: 'Font Awesome 5 Brands'; - font-style: normal; - font-weight: normal; - src: url("../webfonts/fa-brands-400.eot"); - src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } - -.fab { - font-family: 'Font Awesome 5 Brands'; } -@font-face { - font-family: 'Font Awesome 5 Free'; - font-style: normal; - font-weight: 400; - src: url("../webfonts/fa-regular-400.eot"); - src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } - -.far { - font-family: 'Font Awesome 5 Free'; - font-weight: 400; } -@font-face { - font-family: 'Font Awesome 5 Free'; - font-style: normal; - font-weight: 900; - src: url("../webfonts/fa-solid-900.eot"); - src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } - -.fa, -.fas { - font-family: 'Font Awesome 5 Free'; - font-weight: 900; } diff --git a/vendor/fontawesome/css/fontawesome-all.min.css b/vendor/fontawesome/css/fontawesome-all.min.css deleted file mode 100644 index 0bc2ee8..0000000 --- a/vendor/fontawesome/css/fontawesome-all.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:a 2s infinite linear;animation:a 2s infinite linear}.fa-pulse{-webkit-animation:a 1s infinite steps(8);animation:a 1s infinite steps(8)}@-webkit-keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-aws:before{content:"\f375"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blind:before{content:"\f29d"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-briefcase:before{content:"\f0b1"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-car:before{content:"\f1b9"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-certificate:before{content:"\f0a3"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-square:before{content:"\f14a"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comments:before{content:"\f086"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crosshairs:before{content:"\f05b"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-deviantart:before{content:"\f1bd"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-dot-circle:before{content:"\f192"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drupal:before{content:"\f1a9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-excel:before{content:"\f1c3"}.fa-file-image:before{content:"\f1c5"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-firstdraft:before{content:"\f3a1"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frown:before{content:"\f119"}.fa-futbol:before{content:"\f1e3"}.fa-gamepad:before{content:"\f11b"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-gift:before{content:"\f06b"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-martini:before{content:"\f000"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-handshake:before{content:"\f2b5"}.fa-hashtag:before{content:"\f292"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-heart:before{content:"\f004"}.fa-heartbeat:before{content:"\f21e"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-internet-explorer:before{content:"\f26b"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-jenkins:before{content:"\f3b6"}.fa-joget:before{content:"\f3b7"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-key:before{content:"\f084"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-korvue:before{content:"\f42f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-male:before{content:"\f183"}.fa-map:before{content:"\f279"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-maxcdn:before{content:"\f136"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-meh:before{content:"\f11a"}.fa-mercury:before{content:"\f223"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-moon:before{content:"\f186"}.fa-motorcycle:before{content:"\f21c"}.fa-mouse-pointer:before{content:"\f245"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-paragraph:before{content:"\f1dd"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-percent:before{content:"\f295"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phone:before{content:"\f095"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-plane:before{content:"\f072"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-print:before{content:"\f02f"}.fa-product-hunt:before{content:"\f288"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-random:before{content:"\f074"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-rebel:before{content:"\f1d0"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-rendact:before{content:"\f3e4"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-resolving:before{content:"\f3e7"}.fa-retweet:before{content:"\f079"}.fa-road:before{content:"\f018"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-rupee-sign:before{content:"\f156"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-scribd:before{content:"\f28a"}.fa-search:before{content:"\f002"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shower:before{content:"\f2cc"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowflake:before{content:"\f2dc"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spinner:before{content:"\f110"}.fa-spotify:before{content:"\f1bc"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-star:before{content:"\f005"}.fa-star-half:before{content:"\f089"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-strava:before{content:"\f428"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-trademark:before{content:"\f25c"}.fa-train:before{content:"\f238"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-circle:before{content:"\f2bd"}.fa-user-md:before{content:"\f0f0"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vuejs:before{content:"\f41f"}.fa-warehouse:before{content:"\f494"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wrench:before{content:"\f0ad"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands}@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:Font Awesome\ 5 Free}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/vendor/fontawesome/css/fontawesome.css b/vendor/fontawesome/css/fontawesome.css deleted file mode 100644 index 460706e..0000000 --- a/vendor/fontawesome/css/fontawesome.css +++ /dev/null @@ -1,2696 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa, -.fas, -.far, -.fal, -.fab { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-block; - font-style: normal; - font-variant: normal; - text-rendering: auto; - line-height: 1; } - -.fa-lg { - font-size: 1.33333em; - line-height: 0.75em; - vertical-align: -.0667em; } - -.fa-xs { - font-size: .75em; } - -.fa-sm { - font-size: .875em; } - -.fa-1x { - font-size: 1em; } - -.fa-2x { - font-size: 2em; } - -.fa-3x { - font-size: 3em; } - -.fa-4x { - font-size: 4em; } - -.fa-5x { - font-size: 5em; } - -.fa-6x { - font-size: 6em; } - -.fa-7x { - font-size: 7em; } - -.fa-8x { - font-size: 8em; } - -.fa-9x { - font-size: 9em; } - -.fa-10x { - font-size: 10em; } - -.fa-fw { - text-align: center; - width: 1.25em; } - -.fa-ul { - list-style-type: none; - margin-left: 2.5em; - padding-left: 0; } - .fa-ul > li { - position: relative; } - -.fa-li { - left: -2em; - position: absolute; - text-align: center; - width: 2em; - line-height: inherit; } - -.fa-border { - border: solid 0.08em #eee; - border-radius: .1em; - padding: .2em .25em .15em; } - -.fa-pull-left { - float: left; } - -.fa-pull-right { - float: right; } - -.fa.fa-pull-left, -.fas.fa-pull-left, -.far.fa-pull-left, -.fal.fa-pull-left, -.fab.fa-pull-left { - margin-right: .3em; } - -.fa.fa-pull-right, -.fas.fa-pull-right, -.far.fa-pull-right, -.fal.fa-pull-right, -.fab.fa-pull-right { - margin-left: .3em; } - -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; } - -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); } - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - -.fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); } - -.fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - transform: rotate(180deg); } - -.fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - transform: rotate(270deg); } - -.fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); } - -.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - transform: scale(1, -1); } - -.fa-flip-horizontal.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(-1, -1); - transform: scale(-1, -1); } - -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - -webkit-filter: none; - filter: none; } - -.fa-stack { - display: inline-block; - height: 2em; - line-height: 2em; - position: relative; - vertical-align: middle; - width: 2em; } - -.fa-stack-1x, -.fa-stack-2x { - left: 0; - position: absolute; - text-align: center; - width: 100%; } - -.fa-stack-1x { - line-height: inherit; } - -.fa-stack-2x { - font-size: 2em; } - -.fa-inverse { - color: #fff; } - -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen -readers do not read off random characters that represent icons */ -.fa-500px:before { - content: "\f26e"; } - -.fa-accessible-icon:before { - content: "\f368"; } - -.fa-accusoft:before { - content: "\f369"; } - -.fa-address-book:before { - content: "\f2b9"; } - -.fa-address-card:before { - content: "\f2bb"; } - -.fa-adjust:before { - content: "\f042"; } - -.fa-adn:before { - content: "\f170"; } - -.fa-adversal:before { - content: "\f36a"; } - -.fa-affiliatetheme:before { - content: "\f36b"; } - -.fa-algolia:before { - content: "\f36c"; } - -.fa-align-center:before { - content: "\f037"; } - -.fa-align-justify:before { - content: "\f039"; } - -.fa-align-left:before { - content: "\f036"; } - -.fa-align-right:before { - content: "\f038"; } - -.fa-amazon:before { - content: "\f270"; } - -.fa-amazon-pay:before { - content: "\f42c"; } - -.fa-ambulance:before { - content: "\f0f9"; } - -.fa-american-sign-language-interpreting:before { - content: "\f2a3"; } - -.fa-amilia:before { - content: "\f36d"; } - -.fa-anchor:before { - content: "\f13d"; } - -.fa-android:before { - content: "\f17b"; } - -.fa-angellist:before { - content: "\f209"; } - -.fa-angle-double-down:before { - content: "\f103"; } - -.fa-angle-double-left:before { - content: "\f100"; } - -.fa-angle-double-right:before { - content: "\f101"; } - -.fa-angle-double-up:before { - content: "\f102"; } - -.fa-angle-down:before { - content: "\f107"; } - -.fa-angle-left:before { - content: "\f104"; } - -.fa-angle-right:before { - content: "\f105"; } - -.fa-angle-up:before { - content: "\f106"; } - -.fa-angrycreative:before { - content: "\f36e"; } - -.fa-angular:before { - content: "\f420"; } - -.fa-app-store:before { - content: "\f36f"; } - -.fa-app-store-ios:before { - content: "\f370"; } - -.fa-apper:before { - content: "\f371"; } - -.fa-apple:before { - content: "\f179"; } - -.fa-apple-pay:before { - content: "\f415"; } - -.fa-archive:before { - content: "\f187"; } - -.fa-arrow-alt-circle-down:before { - content: "\f358"; } - -.fa-arrow-alt-circle-left:before { - content: "\f359"; } - -.fa-arrow-alt-circle-right:before { - content: "\f35a"; } - -.fa-arrow-alt-circle-up:before { - content: "\f35b"; } - -.fa-arrow-circle-down:before { - content: "\f0ab"; } - -.fa-arrow-circle-left:before { - content: "\f0a8"; } - -.fa-arrow-circle-right:before { - content: "\f0a9"; } - -.fa-arrow-circle-up:before { - content: "\f0aa"; } - -.fa-arrow-down:before { - content: "\f063"; } - -.fa-arrow-left:before { - content: "\f060"; } - -.fa-arrow-right:before { - content: "\f061"; } - -.fa-arrow-up:before { - content: "\f062"; } - -.fa-arrows-alt:before { - content: "\f0b2"; } - -.fa-arrows-alt-h:before { - content: "\f337"; } - -.fa-arrows-alt-v:before { - content: "\f338"; } - -.fa-assistive-listening-systems:before { - content: "\f2a2"; } - -.fa-asterisk:before { - content: "\f069"; } - -.fa-asymmetrik:before { - content: "\f372"; } - -.fa-at:before { - content: "\f1fa"; } - -.fa-audible:before { - content: "\f373"; } - -.fa-audio-description:before { - content: "\f29e"; } - -.fa-autoprefixer:before { - content: "\f41c"; } - -.fa-avianex:before { - content: "\f374"; } - -.fa-aviato:before { - content: "\f421"; } - -.fa-aws:before { - content: "\f375"; } - -.fa-backward:before { - content: "\f04a"; } - -.fa-balance-scale:before { - content: "\f24e"; } - -.fa-ban:before { - content: "\f05e"; } - -.fa-band-aid:before { - content: "\f462"; } - -.fa-bandcamp:before { - content: "\f2d5"; } - -.fa-barcode:before { - content: "\f02a"; } - -.fa-bars:before { - content: "\f0c9"; } - -.fa-baseball-ball:before { - content: "\f433"; } - -.fa-basketball-ball:before { - content: "\f434"; } - -.fa-bath:before { - content: "\f2cd"; } - -.fa-battery-empty:before { - content: "\f244"; } - -.fa-battery-full:before { - content: "\f240"; } - -.fa-battery-half:before { - content: "\f242"; } - -.fa-battery-quarter:before { - content: "\f243"; } - -.fa-battery-three-quarters:before { - content: "\f241"; } - -.fa-bed:before { - content: "\f236"; } - -.fa-beer:before { - content: "\f0fc"; } - -.fa-behance:before { - content: "\f1b4"; } - -.fa-behance-square:before { - content: "\f1b5"; } - -.fa-bell:before { - content: "\f0f3"; } - -.fa-bell-slash:before { - content: "\f1f6"; } - -.fa-bicycle:before { - content: "\f206"; } - -.fa-bimobject:before { - content: "\f378"; } - -.fa-binoculars:before { - content: "\f1e5"; } - -.fa-birthday-cake:before { - content: "\f1fd"; } - -.fa-bitbucket:before { - content: "\f171"; } - -.fa-bitcoin:before { - content: "\f379"; } - -.fa-bity:before { - content: "\f37a"; } - -.fa-black-tie:before { - content: "\f27e"; } - -.fa-blackberry:before { - content: "\f37b"; } - -.fa-blind:before { - content: "\f29d"; } - -.fa-blogger:before { - content: "\f37c"; } - -.fa-blogger-b:before { - content: "\f37d"; } - -.fa-bluetooth:before { - content: "\f293"; } - -.fa-bluetooth-b:before { - content: "\f294"; } - -.fa-bold:before { - content: "\f032"; } - -.fa-bolt:before { - content: "\f0e7"; } - -.fa-bomb:before { - content: "\f1e2"; } - -.fa-book:before { - content: "\f02d"; } - -.fa-bookmark:before { - content: "\f02e"; } - -.fa-bowling-ball:before { - content: "\f436"; } - -.fa-box:before { - content: "\f466"; } - -.fa-boxes:before { - content: "\f468"; } - -.fa-braille:before { - content: "\f2a1"; } - -.fa-briefcase:before { - content: "\f0b1"; } - -.fa-btc:before { - content: "\f15a"; } - -.fa-bug:before { - content: "\f188"; } - -.fa-building:before { - content: "\f1ad"; } - -.fa-bullhorn:before { - content: "\f0a1"; } - -.fa-bullseye:before { - content: "\f140"; } - -.fa-buromobelexperte:before { - content: "\f37f"; } - -.fa-bus:before { - content: "\f207"; } - -.fa-buysellads:before { - content: "\f20d"; } - -.fa-calculator:before { - content: "\f1ec"; } - -.fa-calendar:before { - content: "\f133"; } - -.fa-calendar-alt:before { - content: "\f073"; } - -.fa-calendar-check:before { - content: "\f274"; } - -.fa-calendar-minus:before { - content: "\f272"; } - -.fa-calendar-plus:before { - content: "\f271"; } - -.fa-calendar-times:before { - content: "\f273"; } - -.fa-camera:before { - content: "\f030"; } - -.fa-camera-retro:before { - content: "\f083"; } - -.fa-car:before { - content: "\f1b9"; } - -.fa-caret-down:before { - content: "\f0d7"; } - -.fa-caret-left:before { - content: "\f0d9"; } - -.fa-caret-right:before { - content: "\f0da"; } - -.fa-caret-square-down:before { - content: "\f150"; } - -.fa-caret-square-left:before { - content: "\f191"; } - -.fa-caret-square-right:before { - content: "\f152"; } - -.fa-caret-square-up:before { - content: "\f151"; } - -.fa-caret-up:before { - content: "\f0d8"; } - -.fa-cart-arrow-down:before { - content: "\f218"; } - -.fa-cart-plus:before { - content: "\f217"; } - -.fa-cc-amazon-pay:before { - content: "\f42d"; } - -.fa-cc-amex:before { - content: "\f1f3"; } - -.fa-cc-apple-pay:before { - content: "\f416"; } - -.fa-cc-diners-club:before { - content: "\f24c"; } - -.fa-cc-discover:before { - content: "\f1f2"; } - -.fa-cc-jcb:before { - content: "\f24b"; } - -.fa-cc-mastercard:before { - content: "\f1f1"; } - -.fa-cc-paypal:before { - content: "\f1f4"; } - -.fa-cc-stripe:before { - content: "\f1f5"; } - -.fa-cc-visa:before { - content: "\f1f0"; } - -.fa-centercode:before { - content: "\f380"; } - -.fa-certificate:before { - content: "\f0a3"; } - -.fa-chart-area:before { - content: "\f1fe"; } - -.fa-chart-bar:before { - content: "\f080"; } - -.fa-chart-line:before { - content: "\f201"; } - -.fa-chart-pie:before { - content: "\f200"; } - -.fa-check:before { - content: "\f00c"; } - -.fa-check-circle:before { - content: "\f058"; } - -.fa-check-square:before { - content: "\f14a"; } - -.fa-chess:before { - content: "\f439"; } - -.fa-chess-bishop:before { - content: "\f43a"; } - -.fa-chess-board:before { - content: "\f43c"; } - -.fa-chess-king:before { - content: "\f43f"; } - -.fa-chess-knight:before { - content: "\f441"; } - -.fa-chess-pawn:before { - content: "\f443"; } - -.fa-chess-queen:before { - content: "\f445"; } - -.fa-chess-rook:before { - content: "\f447"; } - -.fa-chevron-circle-down:before { - content: "\f13a"; } - -.fa-chevron-circle-left:before { - content: "\f137"; } - -.fa-chevron-circle-right:before { - content: "\f138"; } - -.fa-chevron-circle-up:before { - content: "\f139"; } - -.fa-chevron-down:before { - content: "\f078"; } - -.fa-chevron-left:before { - content: "\f053"; } - -.fa-chevron-right:before { - content: "\f054"; } - -.fa-chevron-up:before { - content: "\f077"; } - -.fa-child:before { - content: "\f1ae"; } - -.fa-chrome:before { - content: "\f268"; } - -.fa-circle:before { - content: "\f111"; } - -.fa-circle-notch:before { - content: "\f1ce"; } - -.fa-clipboard:before { - content: "\f328"; } - -.fa-clipboard-check:before { - content: "\f46c"; } - -.fa-clipboard-list:before { - content: "\f46d"; } - -.fa-clock:before { - content: "\f017"; } - -.fa-clone:before { - content: "\f24d"; } - -.fa-closed-captioning:before { - content: "\f20a"; } - -.fa-cloud:before { - content: "\f0c2"; } - -.fa-cloud-download-alt:before { - content: "\f381"; } - -.fa-cloud-upload-alt:before { - content: "\f382"; } - -.fa-cloudscale:before { - content: "\f383"; } - -.fa-cloudsmith:before { - content: "\f384"; } - -.fa-cloudversify:before { - content: "\f385"; } - -.fa-code:before { - content: "\f121"; } - -.fa-code-branch:before { - content: "\f126"; } - -.fa-codepen:before { - content: "\f1cb"; } - -.fa-codiepie:before { - content: "\f284"; } - -.fa-coffee:before { - content: "\f0f4"; } - -.fa-cog:before { - content: "\f013"; } - -.fa-cogs:before { - content: "\f085"; } - -.fa-columns:before { - content: "\f0db"; } - -.fa-comment:before { - content: "\f075"; } - -.fa-comment-alt:before { - content: "\f27a"; } - -.fa-comments:before { - content: "\f086"; } - -.fa-compass:before { - content: "\f14e"; } - -.fa-compress:before { - content: "\f066"; } - -.fa-connectdevelop:before { - content: "\f20e"; } - -.fa-contao:before { - content: "\f26d"; } - -.fa-copy:before { - content: "\f0c5"; } - -.fa-copyright:before { - content: "\f1f9"; } - -.fa-cpanel:before { - content: "\f388"; } - -.fa-creative-commons:before { - content: "\f25e"; } - -.fa-credit-card:before { - content: "\f09d"; } - -.fa-crop:before { - content: "\f125"; } - -.fa-crosshairs:before { - content: "\f05b"; } - -.fa-css3:before { - content: "\f13c"; } - -.fa-css3-alt:before { - content: "\f38b"; } - -.fa-cube:before { - content: "\f1b2"; } - -.fa-cubes:before { - content: "\f1b3"; } - -.fa-cut:before { - content: "\f0c4"; } - -.fa-cuttlefish:before { - content: "\f38c"; } - -.fa-d-and-d:before { - content: "\f38d"; } - -.fa-dashcube:before { - content: "\f210"; } - -.fa-database:before { - content: "\f1c0"; } - -.fa-deaf:before { - content: "\f2a4"; } - -.fa-delicious:before { - content: "\f1a5"; } - -.fa-deploydog:before { - content: "\f38e"; } - -.fa-deskpro:before { - content: "\f38f"; } - -.fa-desktop:before { - content: "\f108"; } - -.fa-deviantart:before { - content: "\f1bd"; } - -.fa-digg:before { - content: "\f1a6"; } - -.fa-digital-ocean:before { - content: "\f391"; } - -.fa-discord:before { - content: "\f392"; } - -.fa-discourse:before { - content: "\f393"; } - -.fa-dna:before { - content: "\f471"; } - -.fa-dochub:before { - content: "\f394"; } - -.fa-docker:before { - content: "\f395"; } - -.fa-dollar-sign:before { - content: "\f155"; } - -.fa-dolly:before { - content: "\f472"; } - -.fa-dolly-flatbed:before { - content: "\f474"; } - -.fa-dot-circle:before { - content: "\f192"; } - -.fa-download:before { - content: "\f019"; } - -.fa-draft2digital:before { - content: "\f396"; } - -.fa-dribbble:before { - content: "\f17d"; } - -.fa-dribbble-square:before { - content: "\f397"; } - -.fa-dropbox:before { - content: "\f16b"; } - -.fa-drupal:before { - content: "\f1a9"; } - -.fa-dyalog:before { - content: "\f399"; } - -.fa-earlybirds:before { - content: "\f39a"; } - -.fa-edge:before { - content: "\f282"; } - -.fa-edit:before { - content: "\f044"; } - -.fa-eject:before { - content: "\f052"; } - -.fa-elementor:before { - content: "\f430"; } - -.fa-ellipsis-h:before { - content: "\f141"; } - -.fa-ellipsis-v:before { - content: "\f142"; } - -.fa-ember:before { - content: "\f423"; } - -.fa-empire:before { - content: "\f1d1"; } - -.fa-envelope:before { - content: "\f0e0"; } - -.fa-envelope-open:before { - content: "\f2b6"; } - -.fa-envelope-square:before { - content: "\f199"; } - -.fa-envira:before { - content: "\f299"; } - -.fa-eraser:before { - content: "\f12d"; } - -.fa-erlang:before { - content: "\f39d"; } - -.fa-ethereum:before { - content: "\f42e"; } - -.fa-etsy:before { - content: "\f2d7"; } - -.fa-euro-sign:before { - content: "\f153"; } - -.fa-exchange-alt:before { - content: "\f362"; } - -.fa-exclamation:before { - content: "\f12a"; } - -.fa-exclamation-circle:before { - content: "\f06a"; } - -.fa-exclamation-triangle:before { - content: "\f071"; } - -.fa-expand:before { - content: "\f065"; } - -.fa-expand-arrows-alt:before { - content: "\f31e"; } - -.fa-expeditedssl:before { - content: "\f23e"; } - -.fa-external-link-alt:before { - content: "\f35d"; } - -.fa-external-link-square-alt:before { - content: "\f360"; } - -.fa-eye:before { - content: "\f06e"; } - -.fa-eye-dropper:before { - content: "\f1fb"; } - -.fa-eye-slash:before { - content: "\f070"; } - -.fa-facebook:before { - content: "\f09a"; } - -.fa-facebook-f:before { - content: "\f39e"; } - -.fa-facebook-messenger:before { - content: "\f39f"; } - -.fa-facebook-square:before { - content: "\f082"; } - -.fa-fast-backward:before { - content: "\f049"; } - -.fa-fast-forward:before { - content: "\f050"; } - -.fa-fax:before { - content: "\f1ac"; } - -.fa-female:before { - content: "\f182"; } - -.fa-fighter-jet:before { - content: "\f0fb"; } - -.fa-file:before { - content: "\f15b"; } - -.fa-file-alt:before { - content: "\f15c"; } - -.fa-file-archive:before { - content: "\f1c6"; } - -.fa-file-audio:before { - content: "\f1c7"; } - -.fa-file-code:before { - content: "\f1c9"; } - -.fa-file-excel:before { - content: "\f1c3"; } - -.fa-file-image:before { - content: "\f1c5"; } - -.fa-file-pdf:before { - content: "\f1c1"; } - -.fa-file-powerpoint:before { - content: "\f1c4"; } - -.fa-file-video:before { - content: "\f1c8"; } - -.fa-file-word:before { - content: "\f1c2"; } - -.fa-film:before { - content: "\f008"; } - -.fa-filter:before { - content: "\f0b0"; } - -.fa-fire:before { - content: "\f06d"; } - -.fa-fire-extinguisher:before { - content: "\f134"; } - -.fa-firefox:before { - content: "\f269"; } - -.fa-first-aid:before { - content: "\f479"; } - -.fa-first-order:before { - content: "\f2b0"; } - -.fa-firstdraft:before { - content: "\f3a1"; } - -.fa-flag:before { - content: "\f024"; } - -.fa-flag-checkered:before { - content: "\f11e"; } - -.fa-flask:before { - content: "\f0c3"; } - -.fa-flickr:before { - content: "\f16e"; } - -.fa-flipboard:before { - content: "\f44d"; } - -.fa-fly:before { - content: "\f417"; } - -.fa-folder:before { - content: "\f07b"; } - -.fa-folder-open:before { - content: "\f07c"; } - -.fa-font:before { - content: "\f031"; } - -.fa-font-awesome:before { - content: "\f2b4"; } - -.fa-font-awesome-alt:before { - content: "\f35c"; } - -.fa-font-awesome-flag:before { - content: "\f425"; } - -.fa-fonticons:before { - content: "\f280"; } - -.fa-fonticons-fi:before { - content: "\f3a2"; } - -.fa-football-ball:before { - content: "\f44e"; } - -.fa-fort-awesome:before { - content: "\f286"; } - -.fa-fort-awesome-alt:before { - content: "\f3a3"; } - -.fa-forumbee:before { - content: "\f211"; } - -.fa-forward:before { - content: "\f04e"; } - -.fa-foursquare:before { - content: "\f180"; } - -.fa-free-code-camp:before { - content: "\f2c5"; } - -.fa-freebsd:before { - content: "\f3a4"; } - -.fa-frown:before { - content: "\f119"; } - -.fa-futbol:before { - content: "\f1e3"; } - -.fa-gamepad:before { - content: "\f11b"; } - -.fa-gavel:before { - content: "\f0e3"; } - -.fa-gem:before { - content: "\f3a5"; } - -.fa-genderless:before { - content: "\f22d"; } - -.fa-get-pocket:before { - content: "\f265"; } - -.fa-gg:before { - content: "\f260"; } - -.fa-gg-circle:before { - content: "\f261"; } - -.fa-gift:before { - content: "\f06b"; } - -.fa-git:before { - content: "\f1d3"; } - -.fa-git-square:before { - content: "\f1d2"; } - -.fa-github:before { - content: "\f09b"; } - -.fa-github-alt:before { - content: "\f113"; } - -.fa-github-square:before { - content: "\f092"; } - -.fa-gitkraken:before { - content: "\f3a6"; } - -.fa-gitlab:before { - content: "\f296"; } - -.fa-gitter:before { - content: "\f426"; } - -.fa-glass-martini:before { - content: "\f000"; } - -.fa-glide:before { - content: "\f2a5"; } - -.fa-glide-g:before { - content: "\f2a6"; } - -.fa-globe:before { - content: "\f0ac"; } - -.fa-gofore:before { - content: "\f3a7"; } - -.fa-golf-ball:before { - content: "\f450"; } - -.fa-goodreads:before { - content: "\f3a8"; } - -.fa-goodreads-g:before { - content: "\f3a9"; } - -.fa-google:before { - content: "\f1a0"; } - -.fa-google-drive:before { - content: "\f3aa"; } - -.fa-google-play:before { - content: "\f3ab"; } - -.fa-google-plus:before { - content: "\f2b3"; } - -.fa-google-plus-g:before { - content: "\f0d5"; } - -.fa-google-plus-square:before { - content: "\f0d4"; } - -.fa-google-wallet:before { - content: "\f1ee"; } - -.fa-graduation-cap:before { - content: "\f19d"; } - -.fa-gratipay:before { - content: "\f184"; } - -.fa-grav:before { - content: "\f2d6"; } - -.fa-gripfire:before { - content: "\f3ac"; } - -.fa-grunt:before { - content: "\f3ad"; } - -.fa-gulp:before { - content: "\f3ae"; } - -.fa-h-square:before { - content: "\f0fd"; } - -.fa-hacker-news:before { - content: "\f1d4"; } - -.fa-hacker-news-square:before { - content: "\f3af"; } - -.fa-hand-lizard:before { - content: "\f258"; } - -.fa-hand-paper:before { - content: "\f256"; } - -.fa-hand-peace:before { - content: "\f25b"; } - -.fa-hand-point-down:before { - content: "\f0a7"; } - -.fa-hand-point-left:before { - content: "\f0a5"; } - -.fa-hand-point-right:before { - content: "\f0a4"; } - -.fa-hand-point-up:before { - content: "\f0a6"; } - -.fa-hand-pointer:before { - content: "\f25a"; } - -.fa-hand-rock:before { - content: "\f255"; } - -.fa-hand-scissors:before { - content: "\f257"; } - -.fa-hand-spock:before { - content: "\f259"; } - -.fa-handshake:before { - content: "\f2b5"; } - -.fa-hashtag:before { - content: "\f292"; } - -.fa-hdd:before { - content: "\f0a0"; } - -.fa-heading:before { - content: "\f1dc"; } - -.fa-headphones:before { - content: "\f025"; } - -.fa-heart:before { - content: "\f004"; } - -.fa-heartbeat:before { - content: "\f21e"; } - -.fa-hips:before { - content: "\f452"; } - -.fa-hire-a-helper:before { - content: "\f3b0"; } - -.fa-history:before { - content: "\f1da"; } - -.fa-hockey-puck:before { - content: "\f453"; } - -.fa-home:before { - content: "\f015"; } - -.fa-hooli:before { - content: "\f427"; } - -.fa-hospital:before { - content: "\f0f8"; } - -.fa-hospital-symbol:before { - content: "\f47e"; } - -.fa-hotjar:before { - content: "\f3b1"; } - -.fa-hourglass:before { - content: "\f254"; } - -.fa-hourglass-end:before { - content: "\f253"; } - -.fa-hourglass-half:before { - content: "\f252"; } - -.fa-hourglass-start:before { - content: "\f251"; } - -.fa-houzz:before { - content: "\f27c"; } - -.fa-html5:before { - content: "\f13b"; } - -.fa-hubspot:before { - content: "\f3b2"; } - -.fa-i-cursor:before { - content: "\f246"; } - -.fa-id-badge:before { - content: "\f2c1"; } - -.fa-id-card:before { - content: "\f2c2"; } - -.fa-image:before { - content: "\f03e"; } - -.fa-images:before { - content: "\f302"; } - -.fa-imdb:before { - content: "\f2d8"; } - -.fa-inbox:before { - content: "\f01c"; } - -.fa-indent:before { - content: "\f03c"; } - -.fa-industry:before { - content: "\f275"; } - -.fa-info:before { - content: "\f129"; } - -.fa-info-circle:before { - content: "\f05a"; } - -.fa-instagram:before { - content: "\f16d"; } - -.fa-internet-explorer:before { - content: "\f26b"; } - -.fa-ioxhost:before { - content: "\f208"; } - -.fa-italic:before { - content: "\f033"; } - -.fa-itunes:before { - content: "\f3b4"; } - -.fa-itunes-note:before { - content: "\f3b5"; } - -.fa-jenkins:before { - content: "\f3b6"; } - -.fa-joget:before { - content: "\f3b7"; } - -.fa-joomla:before { - content: "\f1aa"; } - -.fa-js:before { - content: "\f3b8"; } - -.fa-js-square:before { - content: "\f3b9"; } - -.fa-jsfiddle:before { - content: "\f1cc"; } - -.fa-key:before { - content: "\f084"; } - -.fa-keyboard:before { - content: "\f11c"; } - -.fa-keycdn:before { - content: "\f3ba"; } - -.fa-kickstarter:before { - content: "\f3bb"; } - -.fa-kickstarter-k:before { - content: "\f3bc"; } - -.fa-korvue:before { - content: "\f42f"; } - -.fa-language:before { - content: "\f1ab"; } - -.fa-laptop:before { - content: "\f109"; } - -.fa-laravel:before { - content: "\f3bd"; } - -.fa-lastfm:before { - content: "\f202"; } - -.fa-lastfm-square:before { - content: "\f203"; } - -.fa-leaf:before { - content: "\f06c"; } - -.fa-leanpub:before { - content: "\f212"; } - -.fa-lemon:before { - content: "\f094"; } - -.fa-less:before { - content: "\f41d"; } - -.fa-level-down-alt:before { - content: "\f3be"; } - -.fa-level-up-alt:before { - content: "\f3bf"; } - -.fa-life-ring:before { - content: "\f1cd"; } - -.fa-lightbulb:before { - content: "\f0eb"; } - -.fa-line:before { - content: "\f3c0"; } - -.fa-link:before { - content: "\f0c1"; } - -.fa-linkedin:before { - content: "\f08c"; } - -.fa-linkedin-in:before { - content: "\f0e1"; } - -.fa-linode:before { - content: "\f2b8"; } - -.fa-linux:before { - content: "\f17c"; } - -.fa-lira-sign:before { - content: "\f195"; } - -.fa-list:before { - content: "\f03a"; } - -.fa-list-alt:before { - content: "\f022"; } - -.fa-list-ol:before { - content: "\f0cb"; } - -.fa-list-ul:before { - content: "\f0ca"; } - -.fa-location-arrow:before { - content: "\f124"; } - -.fa-lock:before { - content: "\f023"; } - -.fa-lock-open:before { - content: "\f3c1"; } - -.fa-long-arrow-alt-down:before { - content: "\f309"; } - -.fa-long-arrow-alt-left:before { - content: "\f30a"; } - -.fa-long-arrow-alt-right:before { - content: "\f30b"; } - -.fa-long-arrow-alt-up:before { - content: "\f30c"; } - -.fa-low-vision:before { - content: "\f2a8"; } - -.fa-lyft:before { - content: "\f3c3"; } - -.fa-magento:before { - content: "\f3c4"; } - -.fa-magic:before { - content: "\f0d0"; } - -.fa-magnet:before { - content: "\f076"; } - -.fa-male:before { - content: "\f183"; } - -.fa-map:before { - content: "\f279"; } - -.fa-map-marker:before { - content: "\f041"; } - -.fa-map-marker-alt:before { - content: "\f3c5"; } - -.fa-map-pin:before { - content: "\f276"; } - -.fa-map-signs:before { - content: "\f277"; } - -.fa-mars:before { - content: "\f222"; } - -.fa-mars-double:before { - content: "\f227"; } - -.fa-mars-stroke:before { - content: "\f229"; } - -.fa-mars-stroke-h:before { - content: "\f22b"; } - -.fa-mars-stroke-v:before { - content: "\f22a"; } - -.fa-maxcdn:before { - content: "\f136"; } - -.fa-medapps:before { - content: "\f3c6"; } - -.fa-medium:before { - content: "\f23a"; } - -.fa-medium-m:before { - content: "\f3c7"; } - -.fa-medkit:before { - content: "\f0fa"; } - -.fa-medrt:before { - content: "\f3c8"; } - -.fa-meetup:before { - content: "\f2e0"; } - -.fa-meh:before { - content: "\f11a"; } - -.fa-mercury:before { - content: "\f223"; } - -.fa-microchip:before { - content: "\f2db"; } - -.fa-microphone:before { - content: "\f130"; } - -.fa-microphone-slash:before { - content: "\f131"; } - -.fa-microsoft:before { - content: "\f3ca"; } - -.fa-minus:before { - content: "\f068"; } - -.fa-minus-circle:before { - content: "\f056"; } - -.fa-minus-square:before { - content: "\f146"; } - -.fa-mix:before { - content: "\f3cb"; } - -.fa-mixcloud:before { - content: "\f289"; } - -.fa-mizuni:before { - content: "\f3cc"; } - -.fa-mobile:before { - content: "\f10b"; } - -.fa-mobile-alt:before { - content: "\f3cd"; } - -.fa-modx:before { - content: "\f285"; } - -.fa-monero:before { - content: "\f3d0"; } - -.fa-money-bill-alt:before { - content: "\f3d1"; } - -.fa-moon:before { - content: "\f186"; } - -.fa-motorcycle:before { - content: "\f21c"; } - -.fa-mouse-pointer:before { - content: "\f245"; } - -.fa-music:before { - content: "\f001"; } - -.fa-napster:before { - content: "\f3d2"; } - -.fa-neuter:before { - content: "\f22c"; } - -.fa-newspaper:before { - content: "\f1ea"; } - -.fa-nintendo-switch:before { - content: "\f418"; } - -.fa-node:before { - content: "\f419"; } - -.fa-node-js:before { - content: "\f3d3"; } - -.fa-npm:before { - content: "\f3d4"; } - -.fa-ns8:before { - content: "\f3d5"; } - -.fa-nutritionix:before { - content: "\f3d6"; } - -.fa-object-group:before { - content: "\f247"; } - -.fa-object-ungroup:before { - content: "\f248"; } - -.fa-odnoklassniki:before { - content: "\f263"; } - -.fa-odnoklassniki-square:before { - content: "\f264"; } - -.fa-opencart:before { - content: "\f23d"; } - -.fa-openid:before { - content: "\f19b"; } - -.fa-opera:before { - content: "\f26a"; } - -.fa-optin-monster:before { - content: "\f23c"; } - -.fa-osi:before { - content: "\f41a"; } - -.fa-outdent:before { - content: "\f03b"; } - -.fa-page4:before { - content: "\f3d7"; } - -.fa-pagelines:before { - content: "\f18c"; } - -.fa-paint-brush:before { - content: "\f1fc"; } - -.fa-palfed:before { - content: "\f3d8"; } - -.fa-pallet:before { - content: "\f482"; } - -.fa-paper-plane:before { - content: "\f1d8"; } - -.fa-paperclip:before { - content: "\f0c6"; } - -.fa-paragraph:before { - content: "\f1dd"; } - -.fa-paste:before { - content: "\f0ea"; } - -.fa-patreon:before { - content: "\f3d9"; } - -.fa-pause:before { - content: "\f04c"; } - -.fa-pause-circle:before { - content: "\f28b"; } - -.fa-paw:before { - content: "\f1b0"; } - -.fa-paypal:before { - content: "\f1ed"; } - -.fa-pen-square:before { - content: "\f14b"; } - -.fa-pencil-alt:before { - content: "\f303"; } - -.fa-percent:before { - content: "\f295"; } - -.fa-periscope:before { - content: "\f3da"; } - -.fa-phabricator:before { - content: "\f3db"; } - -.fa-phoenix-framework:before { - content: "\f3dc"; } - -.fa-phone:before { - content: "\f095"; } - -.fa-phone-square:before { - content: "\f098"; } - -.fa-phone-volume:before { - content: "\f2a0"; } - -.fa-php:before { - content: "\f457"; } - -.fa-pied-piper:before { - content: "\f2ae"; } - -.fa-pied-piper-alt:before { - content: "\f1a8"; } - -.fa-pied-piper-pp:before { - content: "\f1a7"; } - -.fa-pills:before { - content: "\f484"; } - -.fa-pinterest:before { - content: "\f0d2"; } - -.fa-pinterest-p:before { - content: "\f231"; } - -.fa-pinterest-square:before { - content: "\f0d3"; } - -.fa-plane:before { - content: "\f072"; } - -.fa-play:before { - content: "\f04b"; } - -.fa-play-circle:before { - content: "\f144"; } - -.fa-playstation:before { - content: "\f3df"; } - -.fa-plug:before { - content: "\f1e6"; } - -.fa-plus:before { - content: "\f067"; } - -.fa-plus-circle:before { - content: "\f055"; } - -.fa-plus-square:before { - content: "\f0fe"; } - -.fa-podcast:before { - content: "\f2ce"; } - -.fa-pound-sign:before { - content: "\f154"; } - -.fa-power-off:before { - content: "\f011"; } - -.fa-print:before { - content: "\f02f"; } - -.fa-product-hunt:before { - content: "\f288"; } - -.fa-pushed:before { - content: "\f3e1"; } - -.fa-puzzle-piece:before { - content: "\f12e"; } - -.fa-python:before { - content: "\f3e2"; } - -.fa-qq:before { - content: "\f1d6"; } - -.fa-qrcode:before { - content: "\f029"; } - -.fa-question:before { - content: "\f128"; } - -.fa-question-circle:before { - content: "\f059"; } - -.fa-quidditch:before { - content: "\f458"; } - -.fa-quinscape:before { - content: "\f459"; } - -.fa-quora:before { - content: "\f2c4"; } - -.fa-quote-left:before { - content: "\f10d"; } - -.fa-quote-right:before { - content: "\f10e"; } - -.fa-random:before { - content: "\f074"; } - -.fa-ravelry:before { - content: "\f2d9"; } - -.fa-react:before { - content: "\f41b"; } - -.fa-rebel:before { - content: "\f1d0"; } - -.fa-recycle:before { - content: "\f1b8"; } - -.fa-red-river:before { - content: "\f3e3"; } - -.fa-reddit:before { - content: "\f1a1"; } - -.fa-reddit-alien:before { - content: "\f281"; } - -.fa-reddit-square:before { - content: "\f1a2"; } - -.fa-redo:before { - content: "\f01e"; } - -.fa-redo-alt:before { - content: "\f2f9"; } - -.fa-registered:before { - content: "\f25d"; } - -.fa-rendact:before { - content: "\f3e4"; } - -.fa-renren:before { - content: "\f18b"; } - -.fa-reply:before { - content: "\f3e5"; } - -.fa-reply-all:before { - content: "\f122"; } - -.fa-replyd:before { - content: "\f3e6"; } - -.fa-resolving:before { - content: "\f3e7"; } - -.fa-retweet:before { - content: "\f079"; } - -.fa-road:before { - content: "\f018"; } - -.fa-rocket:before { - content: "\f135"; } - -.fa-rocketchat:before { - content: "\f3e8"; } - -.fa-rockrms:before { - content: "\f3e9"; } - -.fa-rss:before { - content: "\f09e"; } - -.fa-rss-square:before { - content: "\f143"; } - -.fa-ruble-sign:before { - content: "\f158"; } - -.fa-rupee-sign:before { - content: "\f156"; } - -.fa-safari:before { - content: "\f267"; } - -.fa-sass:before { - content: "\f41e"; } - -.fa-save:before { - content: "\f0c7"; } - -.fa-schlix:before { - content: "\f3ea"; } - -.fa-scribd:before { - content: "\f28a"; } - -.fa-search:before { - content: "\f002"; } - -.fa-search-minus:before { - content: "\f010"; } - -.fa-search-plus:before { - content: "\f00e"; } - -.fa-searchengin:before { - content: "\f3eb"; } - -.fa-sellcast:before { - content: "\f2da"; } - -.fa-sellsy:before { - content: "\f213"; } - -.fa-server:before { - content: "\f233"; } - -.fa-servicestack:before { - content: "\f3ec"; } - -.fa-share:before { - content: "\f064"; } - -.fa-share-alt:before { - content: "\f1e0"; } - -.fa-share-alt-square:before { - content: "\f1e1"; } - -.fa-share-square:before { - content: "\f14d"; } - -.fa-shekel-sign:before { - content: "\f20b"; } - -.fa-shield-alt:before { - content: "\f3ed"; } - -.fa-ship:before { - content: "\f21a"; } - -.fa-shipping-fast:before { - content: "\f48b"; } - -.fa-shirtsinbulk:before { - content: "\f214"; } - -.fa-shopping-bag:before { - content: "\f290"; } - -.fa-shopping-basket:before { - content: "\f291"; } - -.fa-shopping-cart:before { - content: "\f07a"; } - -.fa-shower:before { - content: "\f2cc"; } - -.fa-sign-in-alt:before { - content: "\f2f6"; } - -.fa-sign-language:before { - content: "\f2a7"; } - -.fa-sign-out-alt:before { - content: "\f2f5"; } - -.fa-signal:before { - content: "\f012"; } - -.fa-simplybuilt:before { - content: "\f215"; } - -.fa-sistrix:before { - content: "\f3ee"; } - -.fa-sitemap:before { - content: "\f0e8"; } - -.fa-skyatlas:before { - content: "\f216"; } - -.fa-skype:before { - content: "\f17e"; } - -.fa-slack:before { - content: "\f198"; } - -.fa-slack-hash:before { - content: "\f3ef"; } - -.fa-sliders-h:before { - content: "\f1de"; } - -.fa-slideshare:before { - content: "\f1e7"; } - -.fa-smile:before { - content: "\f118"; } - -.fa-snapchat:before { - content: "\f2ab"; } - -.fa-snapchat-ghost:before { - content: "\f2ac"; } - -.fa-snapchat-square:before { - content: "\f2ad"; } - -.fa-snowflake:before { - content: "\f2dc"; } - -.fa-sort:before { - content: "\f0dc"; } - -.fa-sort-alpha-down:before { - content: "\f15d"; } - -.fa-sort-alpha-up:before { - content: "\f15e"; } - -.fa-sort-amount-down:before { - content: "\f160"; } - -.fa-sort-amount-up:before { - content: "\f161"; } - -.fa-sort-down:before { - content: "\f0dd"; } - -.fa-sort-numeric-down:before { - content: "\f162"; } - -.fa-sort-numeric-up:before { - content: "\f163"; } - -.fa-sort-up:before { - content: "\f0de"; } - -.fa-soundcloud:before { - content: "\f1be"; } - -.fa-space-shuttle:before { - content: "\f197"; } - -.fa-speakap:before { - content: "\f3f3"; } - -.fa-spinner:before { - content: "\f110"; } - -.fa-spotify:before { - content: "\f1bc"; } - -.fa-square:before { - content: "\f0c8"; } - -.fa-square-full:before { - content: "\f45c"; } - -.fa-stack-exchange:before { - content: "\f18d"; } - -.fa-stack-overflow:before { - content: "\f16c"; } - -.fa-star:before { - content: "\f005"; } - -.fa-star-half:before { - content: "\f089"; } - -.fa-staylinked:before { - content: "\f3f5"; } - -.fa-steam:before { - content: "\f1b6"; } - -.fa-steam-square:before { - content: "\f1b7"; } - -.fa-steam-symbol:before { - content: "\f3f6"; } - -.fa-step-backward:before { - content: "\f048"; } - -.fa-step-forward:before { - content: "\f051"; } - -.fa-stethoscope:before { - content: "\f0f1"; } - -.fa-sticker-mule:before { - content: "\f3f7"; } - -.fa-sticky-note:before { - content: "\f249"; } - -.fa-stop:before { - content: "\f04d"; } - -.fa-stop-circle:before { - content: "\f28d"; } - -.fa-stopwatch:before { - content: "\f2f2"; } - -.fa-strava:before { - content: "\f428"; } - -.fa-street-view:before { - content: "\f21d"; } - -.fa-strikethrough:before { - content: "\f0cc"; } - -.fa-stripe:before { - content: "\f429"; } - -.fa-stripe-s:before { - content: "\f42a"; } - -.fa-studiovinari:before { - content: "\f3f8"; } - -.fa-stumbleupon:before { - content: "\f1a4"; } - -.fa-stumbleupon-circle:before { - content: "\f1a3"; } - -.fa-subscript:before { - content: "\f12c"; } - -.fa-subway:before { - content: "\f239"; } - -.fa-suitcase:before { - content: "\f0f2"; } - -.fa-sun:before { - content: "\f185"; } - -.fa-superpowers:before { - content: "\f2dd"; } - -.fa-superscript:before { - content: "\f12b"; } - -.fa-supple:before { - content: "\f3f9"; } - -.fa-sync:before { - content: "\f021"; } - -.fa-sync-alt:before { - content: "\f2f1"; } - -.fa-syringe:before { - content: "\f48e"; } - -.fa-table:before { - content: "\f0ce"; } - -.fa-table-tennis:before { - content: "\f45d"; } - -.fa-tablet:before { - content: "\f10a"; } - -.fa-tablet-alt:before { - content: "\f3fa"; } - -.fa-tachometer-alt:before { - content: "\f3fd"; } - -.fa-tag:before { - content: "\f02b"; } - -.fa-tags:before { - content: "\f02c"; } - -.fa-tasks:before { - content: "\f0ae"; } - -.fa-taxi:before { - content: "\f1ba"; } - -.fa-telegram:before { - content: "\f2c6"; } - -.fa-telegram-plane:before { - content: "\f3fe"; } - -.fa-tencent-weibo:before { - content: "\f1d5"; } - -.fa-terminal:before { - content: "\f120"; } - -.fa-text-height:before { - content: "\f034"; } - -.fa-text-width:before { - content: "\f035"; } - -.fa-th:before { - content: "\f00a"; } - -.fa-th-large:before { - content: "\f009"; } - -.fa-th-list:before { - content: "\f00b"; } - -.fa-themeisle:before { - content: "\f2b2"; } - -.fa-thermometer:before { - content: "\f491"; } - -.fa-thermometer-empty:before { - content: "\f2cb"; } - -.fa-thermometer-full:before { - content: "\f2c7"; } - -.fa-thermometer-half:before { - content: "\f2c9"; } - -.fa-thermometer-quarter:before { - content: "\f2ca"; } - -.fa-thermometer-three-quarters:before { - content: "\f2c8"; } - -.fa-thumbs-down:before { - content: "\f165"; } - -.fa-thumbs-up:before { - content: "\f164"; } - -.fa-thumbtack:before { - content: "\f08d"; } - -.fa-ticket-alt:before { - content: "\f3ff"; } - -.fa-times:before { - content: "\f00d"; } - -.fa-times-circle:before { - content: "\f057"; } - -.fa-tint:before { - content: "\f043"; } - -.fa-toggle-off:before { - content: "\f204"; } - -.fa-toggle-on:before { - content: "\f205"; } - -.fa-trademark:before { - content: "\f25c"; } - -.fa-train:before { - content: "\f238"; } - -.fa-transgender:before { - content: "\f224"; } - -.fa-transgender-alt:before { - content: "\f225"; } - -.fa-trash:before { - content: "\f1f8"; } - -.fa-trash-alt:before { - content: "\f2ed"; } - -.fa-tree:before { - content: "\f1bb"; } - -.fa-trello:before { - content: "\f181"; } - -.fa-tripadvisor:before { - content: "\f262"; } - -.fa-trophy:before { - content: "\f091"; } - -.fa-truck:before { - content: "\f0d1"; } - -.fa-tty:before { - content: "\f1e4"; } - -.fa-tumblr:before { - content: "\f173"; } - -.fa-tumblr-square:before { - content: "\f174"; } - -.fa-tv:before { - content: "\f26c"; } - -.fa-twitch:before { - content: "\f1e8"; } - -.fa-twitter:before { - content: "\f099"; } - -.fa-twitter-square:before { - content: "\f081"; } - -.fa-typo3:before { - content: "\f42b"; } - -.fa-uber:before { - content: "\f402"; } - -.fa-uikit:before { - content: "\f403"; } - -.fa-umbrella:before { - content: "\f0e9"; } - -.fa-underline:before { - content: "\f0cd"; } - -.fa-undo:before { - content: "\f0e2"; } - -.fa-undo-alt:before { - content: "\f2ea"; } - -.fa-uniregistry:before { - content: "\f404"; } - -.fa-universal-access:before { - content: "\f29a"; } - -.fa-university:before { - content: "\f19c"; } - -.fa-unlink:before { - content: "\f127"; } - -.fa-unlock:before { - content: "\f09c"; } - -.fa-unlock-alt:before { - content: "\f13e"; } - -.fa-untappd:before { - content: "\f405"; } - -.fa-upload:before { - content: "\f093"; } - -.fa-usb:before { - content: "\f287"; } - -.fa-user:before { - content: "\f007"; } - -.fa-user-circle:before { - content: "\f2bd"; } - -.fa-user-md:before { - content: "\f0f0"; } - -.fa-user-plus:before { - content: "\f234"; } - -.fa-user-secret:before { - content: "\f21b"; } - -.fa-user-times:before { - content: "\f235"; } - -.fa-users:before { - content: "\f0c0"; } - -.fa-ussunnah:before { - content: "\f407"; } - -.fa-utensil-spoon:before { - content: "\f2e5"; } - -.fa-utensils:before { - content: "\f2e7"; } - -.fa-vaadin:before { - content: "\f408"; } - -.fa-venus:before { - content: "\f221"; } - -.fa-venus-double:before { - content: "\f226"; } - -.fa-venus-mars:before { - content: "\f228"; } - -.fa-viacoin:before { - content: "\f237"; } - -.fa-viadeo:before { - content: "\f2a9"; } - -.fa-viadeo-square:before { - content: "\f2aa"; } - -.fa-viber:before { - content: "\f409"; } - -.fa-video:before { - content: "\f03d"; } - -.fa-vimeo:before { - content: "\f40a"; } - -.fa-vimeo-square:before { - content: "\f194"; } - -.fa-vimeo-v:before { - content: "\f27d"; } - -.fa-vine:before { - content: "\f1ca"; } - -.fa-vk:before { - content: "\f189"; } - -.fa-vnv:before { - content: "\f40b"; } - -.fa-volleyball-ball:before { - content: "\f45f"; } - -.fa-volume-down:before { - content: "\f027"; } - -.fa-volume-off:before { - content: "\f026"; } - -.fa-volume-up:before { - content: "\f028"; } - -.fa-vuejs:before { - content: "\f41f"; } - -.fa-warehouse:before { - content: "\f494"; } - -.fa-weibo:before { - content: "\f18a"; } - -.fa-weight:before { - content: "\f496"; } - -.fa-weixin:before { - content: "\f1d7"; } - -.fa-whatsapp:before { - content: "\f232"; } - -.fa-whatsapp-square:before { - content: "\f40c"; } - -.fa-wheelchair:before { - content: "\f193"; } - -.fa-whmcs:before { - content: "\f40d"; } - -.fa-wifi:before { - content: "\f1eb"; } - -.fa-wikipedia-w:before { - content: "\f266"; } - -.fa-window-close:before { - content: "\f410"; } - -.fa-window-maximize:before { - content: "\f2d0"; } - -.fa-window-minimize:before { - content: "\f2d1"; } - -.fa-window-restore:before { - content: "\f2d2"; } - -.fa-windows:before { - content: "\f17a"; } - -.fa-won-sign:before { - content: "\f159"; } - -.fa-wordpress:before { - content: "\f19a"; } - -.fa-wordpress-simple:before { - content: "\f411"; } - -.fa-wpbeginner:before { - content: "\f297"; } - -.fa-wpexplorer:before { - content: "\f2de"; } - -.fa-wpforms:before { - content: "\f298"; } - -.fa-wrench:before { - content: "\f0ad"; } - -.fa-xbox:before { - content: "\f412"; } - -.fa-xing:before { - content: "\f168"; } - -.fa-xing-square:before { - content: "\f169"; } - -.fa-y-combinator:before { - content: "\f23b"; } - -.fa-yahoo:before { - content: "\f19e"; } - -.fa-yandex:before { - content: "\f413"; } - -.fa-yandex-international:before { - content: "\f414"; } - -.fa-yelp:before { - content: "\f1e9"; } - -.fa-yen-sign:before { - content: "\f157"; } - -.fa-yoast:before { - content: "\f2b1"; } - -.fa-youtube:before { - content: "\f167"; } - -.fa-youtube-square:before { - content: "\f431"; } - -.sr-only { - border: 0; - clip: rect(0, 0, 0, 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; } - -.sr-only-focusable:active, .sr-only-focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - width: auto; } diff --git a/vendor/fontawesome/css/fontawesome.min.css b/vendor/fontawesome/css/fontawesome.min.css deleted file mode 100644 index 67ae218..0000000 --- a/vendor/fontawesome/css/fontawesome.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.0.7 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:a 2s infinite linear;animation:a 2s infinite linear}.fa-pulse{-webkit-animation:a 1s infinite steps(8);animation:a 1s infinite steps(8)}@-webkit-keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-aws:before{content:"\f375"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blind:before{content:"\f29d"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-briefcase:before{content:"\f0b1"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-car:before{content:"\f1b9"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-certificate:before{content:"\f0a3"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-square:before{content:"\f14a"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comments:before{content:"\f086"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crosshairs:before{content:"\f05b"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-deviantart:before{content:"\f1bd"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-dot-circle:before{content:"\f192"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drupal:before{content:"\f1a9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-excel:before{content:"\f1c3"}.fa-file-image:before{content:"\f1c5"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-firstdraft:before{content:"\f3a1"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frown:before{content:"\f119"}.fa-futbol:before{content:"\f1e3"}.fa-gamepad:before{content:"\f11b"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-gift:before{content:"\f06b"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-martini:before{content:"\f000"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-handshake:before{content:"\f2b5"}.fa-hashtag:before{content:"\f292"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-heart:before{content:"\f004"}.fa-heartbeat:before{content:"\f21e"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-internet-explorer:before{content:"\f26b"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-jenkins:before{content:"\f3b6"}.fa-joget:before{content:"\f3b7"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-key:before{content:"\f084"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-korvue:before{content:"\f42f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-male:before{content:"\f183"}.fa-map:before{content:"\f279"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-maxcdn:before{content:"\f136"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-meh:before{content:"\f11a"}.fa-mercury:before{content:"\f223"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-moon:before{content:"\f186"}.fa-motorcycle:before{content:"\f21c"}.fa-mouse-pointer:before{content:"\f245"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-paragraph:before{content:"\f1dd"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-percent:before{content:"\f295"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phone:before{content:"\f095"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-plane:before{content:"\f072"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-print:before{content:"\f02f"}.fa-product-hunt:before{content:"\f288"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-random:before{content:"\f074"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-rebel:before{content:"\f1d0"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-rendact:before{content:"\f3e4"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-resolving:before{content:"\f3e7"}.fa-retweet:before{content:"\f079"}.fa-road:before{content:"\f018"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-rupee-sign:before{content:"\f156"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-scribd:before{content:"\f28a"}.fa-search:before{content:"\f002"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shower:before{content:"\f2cc"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowflake:before{content:"\f2dc"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spinner:before{content:"\f110"}.fa-spotify:before{content:"\f1bc"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-star:before{content:"\f005"}.fa-star-half:before{content:"\f089"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-strava:before{content:"\f428"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-trademark:before{content:"\f25c"}.fa-train:before{content:"\f238"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-circle:before{content:"\f2bd"}.fa-user-md:before{content:"\f0f0"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vuejs:before{content:"\f41f"}.fa-warehouse:before{content:"\f494"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wrench:before{content:"\f0ad"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/vendor/fontawesome/webfonts/fa-brands-400.eot b/vendor/fontawesome/webfonts/fa-brands-400.eot deleted file mode 100644 index fa567908436ca24d8e1035fe9a376bc8184b2ed2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98640 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 NwC6@c_tA8g`#%I1p*#Qp diff --git a/vendor/fontawesome/webfonts/fa-brands-400.svg b/vendor/fontawesome/webfonts/fa-brands-400.svg deleted file mode 100644 index 2235f3e..0000000 --- a/vendor/fontawesome/webfonts/fa-brands-400.svg +++ /dev/null @@ -1,1008 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vendor/fontawesome/webfonts/fa-brands-400.ttf b/vendor/fontawesome/webfonts/fa-brands-400.ttf deleted file mode 100644 index 6bef89754e6fba5315b7be815f3e1f5e3d4880b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98404 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%0DtM-YmN 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 diff --git a/vendor/fontawesome/webfonts/fa-regular-400.eot b/vendor/fontawesome/webfonts/fa-regular-400.eot deleted file mode 100644 index e7e47260b7c4162ad4b932fbf43d5fa72f93b824..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31156 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

      ?L$0t1sb*rK5MUyHTMf^uGi)d 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 UPoC!2PW3)$-Qw*06w&?v7gJ!L5&!@I diff --git a/vendor/fontawesome/webfonts/fa-regular-400.svg b/vendor/fontawesome/webfonts/fa-regular-400.svg deleted file mode 100644 index 7dbd736..0000000 --- a/vendor/fontawesome/webfonts/fa-regular-400.svg +++ /dev/null @@ -1,366 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vendor/fontawesome/webfonts/fa-regular-400.ttf b/vendor/fontawesome/webfonts/fa-regular-400.ttf deleted file mode 100644 index d61aeddb267c5f7a2713234e4514d4041fcefc88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30928 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 diff --git a/vendor/fontawesome/webfonts/fa-regular-400.woff b/vendor/fontawesome/webfonts/fa-regular-400.woff deleted file mode 100644 index 8153f4f428e6d27fc7347bdb62f446e950295d35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14712 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 diff --git a/vendor/fontawesome/webfonts/fa-regular-400.woff2 b/vendor/fontawesome/webfonts/fa-regular-400.woff2 deleted file mode 100644 index 44b010abde8e244bddf765e6413e1d43965b41b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12220 zcmV;tFGJ9GPew8T0RR91057}%3jhEB0C>;<0556)0RR9100000000000000000000 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 KXBumcf)4-yb+l6e diff --git a/vendor/fontawesome/webfonts/fa-solid-900.eot b/vendor/fontawesome/webfonts/fa-solid-900.eot deleted file mode 100644 index af7eea47941656ade119f185ef39955cd6a9a605..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106004 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