diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7dca639
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+ branches: [master, main]
+
+jobs:
+ check:
+ name: Test & Build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests and build
+ run: npm run check
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..789ebbf
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,51 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [master, main]
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+
+ deploy:
+ name: Deploy
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
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/.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/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/.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/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/README.md b/README.md
index 22f31ca..e2584de 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,132 @@
# WebDraft
-App for drawing
+
+[](https://github.com/rtcoder/WebDraft/actions/workflows/ci.yml)
+
+A fast, lightweight browser-based image editor built with vanilla TypeScript and the Canvas 2D API — no frameworks, no bloat, just a crisp drawing experience that loads instantly.
+
+---
+
+## Why WebDraft?
+
+- **No installation** — open in any modern browser and start drawing
+- **Offline-ready** — works without an internet connection after the first load
+- **Privacy-first** — your images never leave your machine
+- **Snappy** — built directly on Canvas 2D with zero runtime dependencies
+
+---
+
+## Features
+
+### Drawing tools
+
+| Tool | Description |
+|------|-------------|
+| Brush | Freehand painting with configurable size and hardness |
+| Eraser | Remove pixels with smooth edges |
+| Bucket fill | Flood-fill with tolerance control |
+| Line | Straight lines with adjustable thickness |
+| Rectangle | Outlined or filled rectangles |
+| Ellipse | Outlined or filled ellipses |
+| Move | Reposition the active layer |
+| Eyedropper | Sample any colour from the canvas |
+| Text | Rich text with live HTML editing |
+
+### Brush & style
+- Stroke and fill colour with independent opacity
+- Shadow: enable/disable, colour, blur, X/Y offset
+- Configurable brush size
+- Web sensitivity for pressure-like variation
+
+### Rich text editing
+- Live `contenteditable` editor overlaid on the canvas
+- Floating toolbar: **bold**, *italic*, underline, font family, font size, colour, alignment
+- Edits render to canvas with full word-wrap matching the layer bounds
+
+### Layers
+- Add, delete, rename, and reorder layers
+- Per-layer visibility toggle and opacity control
+- Independent image data per layer — non-destructive editing
+
+### Image operations
+- Resize canvas with custom width/height
+- Invert colours
+- Mirror horizontally or vertically
+- Rotate 90° left or right
+
+### Canvas & viewport
+- Zoom: 10 % – 800 % in discrete steps
+- Ctrl + scroll wheel to zoom in/out
+- Ctrl + `=` / `+` — zoom in
+- Ctrl + `-` — zoom out
+- Ctrl + `0` — reset zoom to 100 %
+- Zoom percentage indicator in the menu bar
+
+### File & export
+- Save/open projects in `.wdraft` format (JSON, preserves all layers and state)
+- Import images onto a new layer (PNG, JPG, …)
+- Export the flattened canvas as PNG
+
+### Undo / redo
+- Full history stack
+- Ctrl + Z — undo
+- Ctrl + Y — redo
+
+### Localisation
+- English and Polish UI
+- Language auto-detected from `navigator.languages`
+- Switcher in the menu bar
+
+---
+
+## Keyboard shortcuts
+
+| Shortcut | Action |
+|----------|--------|
+| Ctrl + Z | Undo |
+| Ctrl + Y | Redo |
+| Ctrl + S | Save project |
+| Ctrl + O | Open project |
+| Ctrl + scroll | Zoom in / out |
+| Ctrl + = / + | Zoom in |
+| Ctrl + - | Zoom out |
+| Ctrl + 0 | Reset zoom |
+
+---
+
+## Getting started
+
+```bash
+git clone https://github.com/rtcoder/WebDraft.git
+cd WebDraft
+npm install
+npm run dev
+```
+
+Then open [http://localhost:5173](http://localhost:5173).
+
+### Build
+
+```bash
+npm run build
+```
+
+### Type-check + build (CI)
+
+```bash
+npm run check
+```
+
+---
+
+## Tech stack
+
+- **TypeScript** — strict, zero-`any` codebase
+- **Vite** — instant HMR dev server and optimised production build
+- **Canvas 2D API** — all rendering, no WebGL
+- **Zero runtime dependencies**
+
+---
+
+## License
+
+MIT
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":";AAKQ,KADH,eACI;EACG,eAAA;EACA,cAAA;EACA,WAAA;EACA,uBAAA;EACA,gBAAA;EACA,YAAA;EACA,kBAAA;;AAPJ,KADH,eACI,QAQG;AARJ,KADH,eACI,QAQe;EACR,WAAA;EACA,kBAAA;EACA,WAAA;EACA,sBAAA;EACA,wBAAA;EACA,eAAA;EACA,YAAA;EACA,iBAAA;;AAhBR,KADH,eACI,QAkBG;EACI,aAAA;;AAnBR,KADH,eACI,QAqBG;EACI,iBAAA;EACA,YAAA;EACA,iBAAA;;AAxBR,KADH,eACI,QA0BG;EACI,WAAA;EACA,YAAA;EACA,gBAAA;;AACA,KA/BX,eACI,QA0BG,KAIK;EACG,WAAA;;AAKR,KArCP,eAoCI,QACI;EACG,iBAAA;EACA,cAAA;;AAFJ,KArCP,eAoCI,QACI,QAGG;EACI,aAAA;;AAJR,KArCP,eAoCI,QACI,QAMG;EACI,cAAA;;AAKR,KAjDP,eAgDI,MACI;EACG,cAAA;;AADJ,KAjDP,eAgDI,MACI,QAEG;EACI,aAAA;;AAHR,KAjDP,eAgDI,MACI,QAKG;EACI,cAAA;;AAKhB,KAAC;EACG,wBAAA;EACA,OAAO,iBAAP;EACA,cAAA;;AACA,KAJH,YAII;EACG,YAAA;EACA,YAAA;EACA,mBAAA;EACA,YAAA;EACA,kBAAA;;AAEJ,KAXH,YAWI;EACG,wBAAA;EACA,YAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,mBAAA;;AAEJ,KAnBH,YAmBI;EACG,YAAA;EACA,YAAA;EACA,mBAAA;EACA,YAAA;;AAEJ,KAzBH,YAyBI;EACG,YAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,mBAAA;;AAEJ,KAhCH,YAgCI;EACG,YAAA;EACA,YAAA;EACA,uBAAA;EACA,yBAAA;EACA,eAAA;EACA,kBAAA;;AAEJ,KAxCH,YAwCI;EACG,YAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,mBAAA;;AAEJ,KA/CH,YA+CI;EACG,aAAA;;AAEJ,KAlDH,YAkDI;EACG,mBAAA;;AAEJ,KArDH,YAqDI;EACG,mBAAA;;AAEJ,KAxDH,YAwDI;EACG,aAAA;;AACA,KA1DP,YAwDI,MAEI;EACG,mBAAA;;AAEJ,KA7DP,YAwDI,MAKI;EACG,mBAAA;;AAEJ,KAhEP,YAwDI,MAQI;EACG,mBAAA;;AAIZ,KAAC;EACG,kBAAA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;;AAEJ,KAAC;EACG,aAAA;;AAEJ,KAAC;EACG,aAAA;;AAEJ,KAAC;EACG,aAAA;;AC/IR;EACI,YAAA;EACA,QAAQ,kBAAR;EACA,mBAAA;EACA,WAAA;;AAJJ,WAKI;EACI,WAAA;EACA,YAAA;EACA,uBAAuB,uBAAvB;EACA,UAAA;EACA,aAAA;;AAVR,WAKI,qBAMI;EACI,WAAA;EACA,WAAA;EACA,YAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,eAAA;EACA,cAAA;;AAnBZ,WAKI,qBAgBI;EACI,WAAA;EACA,YAAA;EACA,WAAA;;AAxBZ,WA2BI;EACI,WAAA;EACA,mBAAA;EACA,WAAA;EACA,UAAA;EACA,cAAA;;AAhCR,WA2BI,OAMI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,WAAA;EACA,eAAA;EACA,kBAAA;EACA,mBAAA;EACA,cAAA;;AAzCZ,WA2BI,OAMI,OASI;AA1CZ,WA2BI,OAMI,OASU;AA1ClB,WA2BI,OAMI,OASgB;EACR,kBAAA;EACA,eAAA;EACA,oBAAA;EACA,OAAA;EACA,MAAA;EACA,kBAAA;;AAhDhB,WA2BI,OAwBI;EACI,WAAA;EACA,WAAA;;AArDZ,WA2BI,OAwBI,WAGI;AAtDZ,WA2BI,OAwBI,WAII;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,WAAA;EACA,cAAA;EACA,eAAA;;AA9DhB,WA2BI,OAwBI,WAGI,aASI;AA/DhB,WA2BI,OAwBI,WAII,YAQI;AA/DhB,WA2BI,OAwBI,WAGI,aASU;AA/DtB,WA2BI,OAwBI,WAII,YAQU;AA/DtB,WA2BI,OAwBI,WAGI,aASgB;AA/D5B,WA2BI,OAwBI,WAII,YAQgB;EACR,eAAA;;AAEJ,WAvCZ,OAwBI,WAGI,aAYK;AAAD,WAvCZ,OAwBI,WAII,YAWK;EACG,cAAA;;AAIJ,WA5CZ,OAwBI,WAmBI,YACK;EACG,eAAA;EACA,cAAA;EACA,mBAAA;;AAIJ,WAnDZ,OAwBI,WA0BI,aACK;EACG,eAAA;EACA,cAAA;EACA,mBAAA;;AAjFpB,WA2BI,OA0DI;EACI,WAAA;EACA,WAAA;;AAvFZ,WA2BI,OA0DI,WAGI;AAxFZ,WA2BI,OA0DI,WAGkB;EACV,WAAA;EACA,iBAAA;EACA,WAAA;EACA,gBAAA;EACA,eAAA;;AA7FhB,WA2BI,OA0DI,WAGI,aAMI;AA9FhB,WA2BI,OA0DI,WAGkB,gBAMV;EACI,eAAA;EACA,WAAA;;AAhGpB,WA2BI,OA0DI,WAcI;EACI,aAAA;;AApGhB,WA2BI,OA4EI;EACI,WAAA;EACA,WAAA;;AAzGZ,WA2BI,OA4EI,aAGI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;;AA/GhB,WA2BI,OA4EI,aAGI,UAMI;EACI,mBAAA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;EACA,WAAA;EACA,yBAAA;;AAEJ,WA7FZ,OA4EI,aAGI,UAcK,aACG;EACI,mBAAA;;AAGR,WAlGZ,OA4EI,aAGI,UAmBK;EACG,aAAA;;AADJ,WAlGZ,OA4EI,aAGI,UAmBK,YAEG;EACI,mBAAA;;AAGR,WAxGZ,OA4EI,aAGI,UAyBK;EACG,aAAA;;AADJ,WAxGZ,OA4EI,aAGI,UAyBK,UAEG;EACI,mBAAA;;AAGR,WA9GZ,OA4EI,aAGI,UA+BK;EACG,cAAA;EACA,mBAAA;;AC3IpB;EACI,aAAA;;AADJ,YAEI;EACI,WAAA;EACA,WAAA;;AAJR,YAEI,WAGI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,WAAA;EACA,cAAA;EACA,eAAA;;AAZZ,YAEI,WAGI,iBAQI;AAbZ,YAEI,WAGI,iBAQU;AAblB,YAEI,WAGI,iBAQgB;EACR,eAAA;;AAEJ,YAdR,WAGI,iBAWK;AAAQ,YAdjB,WAGI,iBAWc;EACN,cAAA;;AAjBhB,YAqBI;EACI,WAAA;EACA,WAAA;;AAvBR,YAqBI,eAGI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,WAAA;EACA,cAAA;EACA,eAAA;;AA/BZ,YAqBI,eAGI,WAQI;AAhCZ,YAqBI,eAGI,WAQU;AAhClB,YAqBI,eAGI,WAQgB;EACR,eAAA;;AAEJ,YAdR,eAGI,WAWK;AAAQ,YAdjB,eAGI,WAWc;EACN,cAAA;;AAMhB;EACI,aAAA;EACA,kBAAA;EACA,UAAA;EACA,yBAAA;EACA,cAAA;EACA,YAAA;;AChDJ,OACI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,WAAA;EACA,eAAA;EACA,kBAAA;EACA,mBAAA;EACA,cAAA;;AATR,OAWI;EACI,WAAA;EACA,mBAAA;EACA,WAAA;EACA,UAAA;EACA,cAAA;;AAhBR,OAWI,YAMI;EACI,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,WAAA;EACA,cAAA;EACA,eAAA;;AACA,OAdR,YAMI,WAQK;EACG,cAAA;;AAEJ,OAjBR,YAMI,WAWK;EACG,cAAA;;AAEJ,OApBR,YAMI,WAcK;EACG,cAAA;;AAhChB,OAWI,YAMI,WAiBI;AAlCZ,OAWI,YAMI,WAiBU;AAlClB,OAWI,YAMI,WAiBgB;EACR,eAAA;;AAnChB,OAuCI;EACI,WAAA;EACA,mBAAA;EACA,WAAA;EACA,UAAA;EACA,cAAA;EACA,YAAY,kBAAZ;EACA,cAAA;EACA,SAAA;;AA/CR,OAuCI,YASI;EACI,kBAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,uBAAA;EAEA,kBAAA;EACA,eAAA;;AACA,OAlBR,YASI,WASK;EACG,eAAA;EACA,oCAAA;;AACA,OArBZ,YASI,WASK,OAGI;EACG,eAAA;EACA,oCAAA;;AALR,OAlBR,YASI,WASK,OAOG;EACI,qBAAA;;AAGR,OA7BR,YASI,WAoBK;EACG,oCAAA;;AArEhB,OAuCI,YASI,WAuBI;EACI,WAAA;EACA,yBAAA;EACA,WAAA;EACA,YAAA;;AA3EhB,OAuCI,YASI,WAuBI,mBAKI,IAAG;EACC,WAAA;EACA,eAAA;EACA,gBAAA;;AA/EpB,OAuCI,YASI,WAkCI;AAlFZ,OAuCI,YASI,WAkCgB;EACR,WAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,4BAAA;EACA,kBAAA;EACA,eAAA;;AAzFhB,OAuCI,YASI,WA4CI;EACI,cAAA;;AACA,OAvDZ,YASI,WA4CI,WAEK;EACG,cAAA;;AA/FpB,OAuCI,YASI,WAkDI;EACI,cAAA;;AACA,OA7DZ,YASI,WAkDI,WAEK;EACG,cAAA;;ACrGpB;EACI,WAAA;EACA,WAAA;;AAFJ,QAGI;EACI,WAAA;EACA,YAAA;EACA,WAAA;;AACA,QAJJ,iBAIK;EACG,aAAA;;AAEJ,QAPJ,iBAOK;EACG,aAAA;;AAEJ,QAVJ,iBAUK;EACG,aAAA;;AAdZ,QAGI,iBAaI;EACI,WAAA;EACA,YAAA;EACA,eAAA;EACA,iBAAA;EACA,qBAAA;EACA,eAAA;EACA,WAAA;EACA,kBAAA;;ACxBZ;EACI,YAAA;EACA,aAAA;EACA,UAAA;EACA,kBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,8BAAA;EACA,cAAA;EACA,QAAA;EACA,UAAA;;AACA,gBAAiC;EA+CpC;IA9CO,YAAA;;;AAbR,KAeI;EACI,kBAAA;EACA,QAAA;EACA,eAAA;EACA,WAAA;;AAnBR,KAqBI;EACI,WAAA;EACA,WAAA;EACA,eAAA;;AAxBR,KAqBI,cAII;EACI,WAAA;EACA,WAAA;EACA,kBAAA;;AA5BZ,KAqBI,cASI;EACI,WAAA;EACA,WAAA;;AAhCZ,KAqBI,cASI,mBAGI;EACI,UAAA;EACA,WAAA;EACA,gBAAA;EACA,iBAAA;;AArChB,KAqBI,cASI,mBAGI,uBAKI;EACI,cAAA;EACA,eAAA;EACA,YAAA;EACA,kBAAA;EACA,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;EACA,cAAA;;AAjDpB,KAqBI,cASI,mBAsBI;EACI,UAAA;EACA,WAAA;EACA,gBAAA;;ACtDhB;EACI,YAAA;EACA,aAAA;EACA,kBAAA;EACA,MAAA;EACA,OAAA;EACA,mBAAA;EACA,aAAA;EACA,aAAA;EACA,UAAA;EACA,aAAa,yBAAb;EACA,YAAY,yBAAZ;;AAXJ,QAYI,MAAK;EACD,YAAA;EACA,YAAA;EACA,iBAAA;EACA,YAAA;EACA,YAAA;EACA,kBAAA;EACA,WAAA;;AAnBR,QAqBI;EACI,YAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,cAAA;EACA,WAAA;EACA,gBAAA;;AA5BR,QA8BI;EACI,WAAA;EACA,YAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;EACA,mBAAA;EACA,eAAA;;AACA,QAVJ,cAUK;EACG,mBAAA;EACA,cAAA;;AC1CZ;EACI,YAAA;EACA,YAAA;EACA,kBAAA;EACA,YAAA;EACA,OAAA;EACA,mBAAA;EACA,aAAA;;AAPJ,KAQI;EACI,WAAA;EACA,YAAA;EACA,WAAA;EACA,aAAA;;AAZR,KAQI,WAKI;EACI,WAAA;EACA,cAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;EACA,gBAAA;EACA,cAAA;EACA,kBAAA;EACA,iBAAA;EACA,eAAA;;AA1BZ,KAQI,WAoBI;EACI,WAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,iBAAA;EACA,cAAA;EACA,eAAA;;ACnCZ;EACI,YAAA;EACA,aAAA;EACA,kBAAA;EACA,8BAAA;EACA,WAAA;EACA,aAAA;;AANJ,aAOI;EACI,YAAA;EACA,mBAAA;EACA,mBAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;EACA,SAAA;EACA,UAAA;EACA,UAAA;;AAEJ,aAAC;AAAS,aAAC;EACP,kBAAA;EACA,SAAS,GAAT;EACA,mBAAA;EACA,UAAA;;AAEJ,aAAC;EACG,YAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;;AAEJ,aAAC;EACG,UAAA;EACA,aAAA;EACA,OAAA;EACA,SAAA;;ACnCR;EACI,WAAA;EACA,mBAAA;EACA,YAAA;EACA,iBAAA;EACA,WAAA;EACA,kBAAA;EACA,SAAA;EACA,eAAA;EACA,cAAA;;AATJ,UAUI;EACI,WAAA;EACA,YAAA;EACA,eAAA;EACA,oBAAA;EACA,kBAAA;;AAfR,UAiBI;EACI,eAAA;EACA,oBAAA;EACA,YAAA;EACA,QAAA;EACA,kBAAA;;AAtBR,UAiBI,QAMI;EACI,cAAA;EACA,eAAA;;AC1BZ;EACE,mCAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,iBAAA;EACA,mBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAW,qBAAX;EACA,kBAAA;EACA,WAAA;EACA,aAAA;;AACA,OAAC;EACC,cAAA;;AAEF,OAAC;EACC,YAAA;EACA,aAAA;EACA,MAAA;EACA,OAAA;EACA,WAAW,eAAX;;AALF,OAAC,WAMC,eACE;EACE,YAAA;EACA,2BAAA;;AATN,OAAC,WAMC,eAKE;EACE,YAAA;EACA,YAAA;EACA,iBAAA;;AAdN,OAAC,WAMC,eAKE,SAIE,MACE,KAAI;EACF,gBAAA;;AAjCZ,OAuCE;EACE,WAAA;EACA,YAAA;;AAzCJ,OAuCE,cAGE;EACE,wBAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,gBAAA;EACA,cAAA;;AAhDN,OAuCE,cAWE;EACE,WAAA;EACA,YAAA;EACA,YAAA;EACA,qBAAA;EACA,eAAA;EACA,YAAA;;AAxDN,OAuCE,cAWE,OAOE;EACE,eAAA;;AAEF,OArBJ,cAWE,OAUG;EACC,mBAAA;EACA,cAAA;;AAEF,OAzBJ,cAWE,OAcG;EACC,mBAAA;EACA,cAAA;EACA,kBAAA;;AAnER,OAuEE;EACE,WAAA;EACA,WAAA;EACA,gBAAA;;AA1EJ,OAuEE,eAIE;EACE,yBAAA;EACA,WAAA;EACA,kBAAA;;AA9EN,OAuEE,eAIE,gBAIE,OAAM;AA/EZ,OAuEE,eAIE,gBAKE,OAAM;AAhFZ,OAuEE,eAIE,gBAME,OAAM;AAjFZ,OAuEE,eAIE,gBAOE;EACE,WAAA;EACA,gBAAA;EACA,+BAAA;EACA,WAAA;;AAtFR,OAuEE,eAIE,gBAaE,OAAM;AAxFZ,OAuEE,eAIE,gBAcE,OAAM;EACJ,aAAA;;AA1FR,OAuEE,eAIE,gBAiBE,OAAM;EACJ,cAAA;EACA,kBAAA;EACA,MAAA;EACA,OAAA;;AAhGR,OAuEE,eA4BE;EACE,YAAA;EACA,WAAA;;AArGN,OAuEE,eA4BE,SAGE,MACE;EACE,aAAA;;AACA,OAlCR,eA4BE,SAGE,MACE,MAEG,QAAS;EACR,mBAAA;EACA,cAAA;;AA3GZ,OAuEE,eA4BE,SAGE,MAQE,KAAI;EACF,WAAA;EACA,YAAA;EACA,oBAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,eAAA;EACA,WAAA;;AAvHV,OAuEE,eAoDE;EACE,WAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;;AA/HN,OAuEE,eAoDE,eAKE;EACE,eAAA;EACA,YAAA;EACA,aAAA;EACA,eAAA;;AApIR,OAuEE,eAoDE,eAKE,OAKE;EACE,eAAA;;AAEF,OAjEN,eAoDE,eAKE,OAQG;EACC,qBAAA;;AAEF,OApEN,eAoDE,eAKE,OAWG;EACC,mBAAA;EACA,cAAA;;AAEF,OAxEN,eAoDE,eAKE,OAeG;EACC,mBAAA;EACA,cAAA;;AAEF,OA5EN,eAoDE,eAKE,OAmBG;EACC,mBAAA;EACA,cAAA;;AAOR,OAAC,QAEC,eACE;EACE,qBAAA;;AACA,OALL,QAEC,eACE,OAEG;EACC,aAAA;;AANR,OAAC,QAUC,eACE,gBACE,OAAM;AAZZ,OAAC,QAUC,eACE,gBAEE,MAAK;EACH,aAAA;;AAdR,OAAC,QAUC,eACE,gBAKE,OAAM;EACJ,cAAA;;AC7KV,IAAI;EACA,aAAA;EACA,YAAA;EACA,YAAA;EACA,eAAA;EACA,QAAA;EACA,SAAA;EACA,gBAAA;EACA,WAAA;;AARJ,IAAI,YASA,GACI;EACI,eAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,WAAA;EACA,gBAAA;EACA,cAAA;;AAlBZ,IAAI,YASA,GACI,GASI;EACI,eAAA;;AApBhB,IAAI,YASA,GACI,GAYI;EACI,WAAA;EACA,WAAA;;AAxBhB,IAAI,YASA,GACI,GAYI,SAGI;EACI,eAAA;EACA,WAAA;;AACA,IA5BhB,YASA,GACI,GAYI,SAGI,KAGK;EACG,WAAA;;AADJ,IA5BhB,YASA,GACI,GAYI,SAGI,KAGK,KAEG;EACI,WAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;;AAGR,IArChB,YASA,GACI,GAYI,SAGI,KAYK;EACG,yBAAA;;AAEJ,IAxChB,YASA,GACI,GAYI,SAGI,KAeK;EACG,iBAAA;EACA,yBAAA;EACA,WAAA;EACA,eAAA;EACA,cAAA;;AAEJ,IA/ChB,YASA,GACI,GAYI,SAGI,KAsBK;EACG,cAAA;EACA,WAAA;EACA,eAAA;EACA,eAAA;EACA,kBAAA;EACA,UAAA;;AArDxB,IAAI,YASA,GACI,GA+CI;EACI,YAAA;EACA,kBAAA;EACA,aAAA;EACA,MAAA;EACA,WAAA;;AAIY,IAlExB,YASA,GACI,GA+CI,SAMK,WACG,KACK,MACI;EACG,kBAAA;EACA,UAAA;;AAEJ,IAtExB,YASA,GACI,GA+CI,SAMK,WACG,KACK,MAKI;EACG,mBAAA;EACA,UAAA;EACA,YAAA;;AAMpB,IA/ER,YASA,GACI,GAqEK;EACG,cAAA;;AAEJ,IAlFR,YASA,GACI,GAwEK,IAAI,WAAW;EACZ,cAAA;EACA,yBAAA;;AAFJ,IAlFR,YASA,GACI,GAwEK,IAAI,WAAW,MAGZ;EACI,cAAA;EACA,cAAA;;AAKhB,IA5FA,YA4FC,aACG,GACI,GACI;EACI,YAAA;;AAIY,IApG5B,YA4FC,aACG,GACI,GACI,SAEK,WACG,KACK,MACI;EACG,mBAAA;EACA,UAAA;;AAEJ,IAxG5B,YA4FC,aACG,GACI,GACI,SAEK,WACG,KACK,MAKI;EACG,kBAAA;EACA,UAAA;;AC7FpC;EACI,sBAAA;EACA,yBAAA;EACA,qBAAA;EACA,iBAAA;EACA,eAAA;EACA,aAAa,uBAAb;EACA,sBAAA;EACA,2BAAA;EACA,8BAAA;EACA,SAAA;EACA,UAAA;;AAEJ;EACI,gBAAA;;AAGJ,IACI;EACI,MAAA;EACA,OAAA;EACA,WAAA;EACA,YAAA;EACA,WAAA;EACA,eAAA;EACA,gBAAA;;AARR,IACI,KAQI;EACI,WAAA;EACA,mBAAA;EACA,cAAA;EACA,OAAO,mBAAP;EACA,QAAQ,kBAAR;;AAdZ,IACI,KAQI,SAMI;EACI,0BAAA;EACA,kBAAA;EACA,gBAAA;;AAlBhB,IACI,KAQI,SAMI,aAII;AAnBhB,IACI,KAQI,SAMI,aAIW;AAnBvB,IACI,KAQI,SAMI,aAIyB;EACjB,YAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;;;AAvBpB,IACI,KAQI,SAMI,aAWI;EACI,UAAA;EACA,kBAAA;;AA5BpB,IACI,KAQI,SAMI,aAeI,MACI;EACI,kBAAA;EACA,iBAAA;EACA,0BAAA;;AACA,IAlCpB,KAQI,SAMI,aAeI,MACI,OAIK;EACG,kBAAA;;AApC5B,IACI,KAyCI;AA1CR,IACI,KA0CI;AA3CR,IACI,KA2CI;AA5CR,IACI,KA4CI;EACI,aAAA;EACA,kBAAA;EACA,UAAA;EACA,yBAAA;;AAjDZ,IACI,KAkDI;EACI,YAAA;;AApDZ,IACI,KAqDI;EACI,mBAAA;EACA,qBAAA"}
\ 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 = '';
- text += ' ';
- icon += '> ';
- return '' + 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/docs/manual-qa-checklist.md b/docs/manual-qa-checklist.md
new file mode 100644
index 0000000..e779887
--- /dev/null
+++ b/docs/manual-qa-checklist.md
@@ -0,0 +1,90 @@
+# 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.
+- [ ] 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
+
+- [ ] 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`, `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.
+- [ ] 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
new file mode 100644
index 0000000..d2c96db
--- /dev/null
+++ b/docs/rewrite-checklist.md
@@ -0,0 +1,44 @@
+# 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.
+- [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.
+- [x] Add selection, copy, cut, paste.
+- [x] Add shape tools.
+- [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.
+- [x] Add keyboard shortcuts.
+- [x] Add responsive layout polish.
+- [x] 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.
+
+## Release Readiness
+
+- Use `docs/manual-qa-checklist.md` before replacing the old entrypoint or after larger editor changes.
diff --git a/docs/wdraft-format.md b/docs/wdraft-format.md
new file mode 100644
index 0000000..8386936
--- /dev/null
+++ b/docs/wdraft-format.md
@@ -0,0 +1,82 @@
+# `.wdraft` File Format (v2)
+
+## 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:** binary (little-endian)
+
+---
+
+## 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
+
+Without base64 overhead, files are ~25–33% smaller than a JSON equivalent.
+
+| Canvas size | Layers | Estimated file size |
+|-------------|--------|---------------------|
+| 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
+
+- 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` binary file and downloads it.
+
+**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.
diff --git a/favicon.ico b/favicon.ico
deleted file mode 100644
index 12b19e5..0000000
Binary files a/favicon.ico and /dev/null differ
diff --git a/index.html b/index.html
index 7968044..fec3992 100644
--- a/index.html
+++ b/index.html
@@ -1,80 +1,13 @@
-
+
-
+
+
+
+
WebDraft
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Del
-
Delete
-
Ctrl + X
-
Cut
-
Ctrl + C
-
Copy
-
Ctrl + V
-
Paste
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
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 = '';
- text += ' ';
- icon += '> ';
- return '' + 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..952db92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,1547 +1,888 @@
{
- "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"
- }
- },
- "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==",
- "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=",
- "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=",
- "optional": true,
- "requires": {
- "boom": "2.10.1"
+ "packages": {
+ "": {
+ "name": "webdraft-v3",
+ "version": "0.1.0",
+ "devDependencies": {
+ "typescript": "^5.5.4",
+ "vite": "8.0.5"
}
},
- "currently-unhandled": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
- "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "node_modules/@emnapi/core": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
+ "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
"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=",
+ "license": "MIT",
"optional": true,
- "requires": {
- "assert-plus": "1.0.0"
- },
+ "peer": true,
"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
- }
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
}
},
- "dateformat": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
- "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
+ "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
"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=",
- "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==",
+ "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"
- },
+ "peer": true,
"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=",
- "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=",
- "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
- }
+ "tslib": "^2.4.0"
}
},
- "glob": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
- "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"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"
- },
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
"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"
- }
- }
+ "tslib": "^2.4.0"
}
},
- "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=",
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"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"
- },
+ "license": "MIT",
+ "optional": true,
"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"
+ "@tybys/wasm-util": "^0.10.2"
},
- "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"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
},
- "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"
- }
- }
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
}
},
- "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==",
+ "node_modules/@oxc-project/types": {
+ "version": "0.122.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"dev": true,
- "requires": {
- "colors": "1.1.2",
- "grunt-legacy-log-utils": "1.0.0",
- "hooker": "0.2.3",
- "lodash": "4.17.10"
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
}
},
- "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=",
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "requires": {
- "chalk": "1.1.3",
- "lodash": "4.3.0"
- },
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+ "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
"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"
+ "@napi-rs/wasm-runtime": "^1.1.1"
},
- "dependencies": {
- "lodash": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz",
- "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=",
- "dev": true
- }
+ "engines": {
+ "node": ">=14.0.0"
}
},
- "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=",
- "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=",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "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": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
- "requires": {
- "assert-plus": "0.2.0",
- "jsprim": "1.4.1",
- "sshpk": "1.14.1"
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "iconv-lite": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
- "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
"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
+ "license": "MIT"
},
- "indent-string": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
- "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"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"
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "is-finite": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
- "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
- "requires": {
- "number-is-nan": "1.0.1"
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
}
},
- "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=",
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"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=",
- "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=",
- "optional": true,
- "requires": {
- "assert-plus": "1.0.0",
- "extsprintf": "1.3.0",
- "json-schema": "0.2.3",
- "verror": "1.10.0"
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.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=",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
"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=",
+ "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,
- "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"
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "meow": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
- "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"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=",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
"optional": true,
- "requires": {
- "minimist": "0.0.8"
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
},
- "dependencies": {
- "minimist": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "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=",
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
"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"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "parse-json": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
- "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "requires": {
- "error-ex": "1.3.1"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "path-exists": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
- "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "requires": {
- "pinkie-promise": "2.0.1"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "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=",
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "requires": {
- "graceful-fs": "4.1.11",
- "pify": "2.3.0",
- "pinkie-promise": "2.0.1"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "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=",
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
"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==",
+ "license": "MPL-2.0",
"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"
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "read-pkg": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
- "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "requires": {
- "load-json-file": "1.1.0",
- "normalize-package-data": "2.4.0",
- "path-type": "1.1.0"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "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=",
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "requires": {
- "find-up": "1.1.2",
- "read-pkg": "1.1.0"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "redent": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
- "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "requires": {
- "indent-string": "2.1.0",
- "strip-indent": "1.0.1"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "repeating": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
- "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
"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=",
+ "license": "MPL-2.0",
"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"
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "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=",
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
"optional": true,
- "requires": {
- "hoek": "2.16.3"
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
- },
- "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/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,
- "requires": {
- "spdx-expression-parse": "3.0.0",
- "spdx-license-ids": "3.0.0"
+ "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"
}
},
- "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
- },
- "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/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"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
+ "license": "ISC"
},
- "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"
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
},
- "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
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "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"
}
},
- "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=",
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+ "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
"dev": true,
- "requires": {
- "is-utf8": "0.2.1"
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.122.0",
+ "@rolldown/pluginutils": "1.0.0-rc.12"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+ }
+ },
+ "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/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "strip-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
- "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"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": {
- "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"
- }
- },
- "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==",
+ "license": "0BSD",
"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/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
- "requires": {
- "spdx-correct": "3.0.0",
- "spdx-expression-parse": "3.0.0"
+ "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": "8.0.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz",
+ "integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==",
+ "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=",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.12",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
"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..d9a6c9d 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": "8.0.5"
}
}
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 @@
-
-
\ 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
-
-
-
-
-
- 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 @@
-
-
-
-Resize all layers?
-
-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 @@
-
\ 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 @@
-
-
-
- Arial
- Calibri
- Century Gothic
- Comic Sans MS
- Sans serif
- Times New Roman
- Verdana
-
-
- 8
- 10
- 12
- 15
- 18
- 20
- 25
- 30
- 35
- 45
- 50
- 60
- 70
-
\ No newline at end of file
diff --git a/pic/transparent.png b/pic/transparent.png
deleted file mode 100644
index 01dc43d..0000000
Binary files a/pic/transparent.png and /dev/null differ
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..759b9c0
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/canvas-drawing.ts b/src/core/canvas-drawing.ts
new file mode 100644
index 0000000..b6aaece
--- /dev/null
+++ b/src/core/canvas-drawing.ts
@@ -0,0 +1,146 @@
+import type {EditorState, Point, SizeWithPosition} from '../types';
+import {Tool} from '../types';
+
+export {drawRichText} from './rich-text-renderer';
+
+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 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/src/core/editor-geometry.ts b/src/core/editor-geometry.ts
new file mode 100644
index 0000000..454957c
--- /dev/null
+++ b/src/core/editor-geometry.ts
@@ -0,0 +1,66 @@
+import type {ClippedPasteBounds, NaturalSize, Point, Size, SizeWithPosition} from '../types';
+
+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};
+}
+
+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/src/core/flood-fill.ts b/src/core/flood-fill.ts
new file mode 100644
index 0000000..0b7b10e
--- /dev/null
+++ b/src/core/flood-fill.ts
@@ -0,0 +1,103 @@
+import type {RgbaColor} from '../types';
+
+export function floodFillImageData(
+ imageData: ImageData,
+ x: number,
+ y: number,
+ color: RgbaColor,
+ tolerance = 0,
+): 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, 0)) {
+ return false;
+ }
+
+ 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];
+
+ if (currentX < 0 || currentY < 0 || currentX >= imageData.width || currentY >= imageData.height) {
+ continue;
+ }
+
+ const visitIndex = currentY * imageData.width + currentX;
+
+ if (visited[visitIndex]) {
+ continue;
+ }
+
+ visited[visitIndex] = 1;
+
+ if (!colorsMatch(getPixel(imageData, currentX, currentY), target, tolerance)) {
+ 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, tolerance: number): boolean {
+ const threshold = Math.min(Math.max(tolerance, 0), 255);
+
+ return (
+ 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
+ );
+}
+
+function getPixelIndex(width: number, x: number, y: number): number {
+ return (y * width + x) * 4;
+}
diff --git a/src/core/history-manager.ts b/src/core/history-manager.ts
new file mode 100644
index 0000000..4f6d00f
--- /dev/null
+++ b/src/core/history-manager.ts
@@ -0,0 +1,58 @@
+import {HistoryEntry} from '../types';
+
+export class HistoryManager {
+ private readonly undoStack: HistoryEntry[] = [];
+ private readonly redoStack: HistoryEntry[] = [];
+
+ constructor(private readonly limit = 30) {}
+
+ 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.trimUndoStack();
+ 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);
+ 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/src/core/i18n.ts b/src/core/i18n.ts
new file mode 100644
index 0000000..1ae71fe
--- /dev/null
+++ b/src/core/i18n.ts
@@ -0,0 +1,22 @@
+import { en } from '../i18n/en';
+import { pl } from '../i18n/pl';
+import type {Lang, Translations} from '../types';
+
+const LANG_KEY = 'webdraft-lang';
+
+const translations: Record = { 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/src/core/layer-manager.ts b/src/core/layer-manager.ts
new file mode 100644
index 0000000..037d63f
--- /dev/null
+++ b/src/core/layer-manager.ts
@@ -0,0 +1,307 @@
+import type {Layer, LayerDocumentSnapshot, LayerSummary, SizeWithPosition} from '../types';
+import {t} from './i18n.ts';
+
+
+export class LayerManager {
+ private readonly root: HTMLElement;
+ private readonly layers: Layer[] = [];
+ private activeLayerId = '';
+ private layerCount = 0;
+
+ 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;
+ }
+
+ 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,
+ preview: layer.canvas.toDataURL('image/png'),
+ visible: layer.visible,
+ isTextLayer: !!layer.textData,
+ }))
+ .reverse();
+ }
+
+ get visibleLayers(): Layer[] {
+ 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');
+
+ if (!context) {
+ throw new Error('Canvas 2D context is unavailable.');
+ }
+
+ canvas.width = width;
+ canvas.height = height;
+ const id = crypto.randomUUID();
+
+ canvas.className = 'drawing-layer';
+ canvas.dataset.layerId = id;
+
+ const name = t.layers.layerName(`${this.layerCount + 1}`);
+
+ const layer: Layer = {
+ id,
+ name,
+ canvas,
+ context,
+ 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;
+ }
+
+ clearActiveLayer(): void {
+ const layer = this.activeLayer;
+ 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);
+ layer.x = 0;
+ layer.y = 0;
+ this.syncLayerStyle(layer);
+ }
+ }
+
+ captureDocument(): LayerDocumentSnapshot {
+ return {
+ activeLayerId: this.activeLayerId,
+ layerCount: this.layerCount,
+ layers: this.layers.map((layer) => ({
+ layerId: layer.id,
+ name: layer.name,
+ visible: layer.visible,
+ x: layer.x,
+ y: layer.y,
+ textData: layer.textData,
+ imageData: layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height),
+ })),
+ };
+ }
+
+ restoreDocument(snapshot: LayerDocumentSnapshot): void {
+ for (const layer of this.layers) {
+ layer.canvas.remove();
+ }
+
+ 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);
+
+ const layer: Layer = {
+ id: layerSnapshot.layerId,
+ name: layerSnapshot.name,
+ canvas,
+ context,
+ visible: layerSnapshot.visible,
+ x: layerSnapshot.x ?? 0,
+ y: layerSnapshot.y ?? 0,
+ textData: layerSnapshot.textData,
+ };
+ 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();
+ }
+
+ 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;
+ this.updateActiveLayerClass();
+ }
+
+ renameLayer(id: string, name: string): boolean {
+ const layer = this.assertLayer(id);
+ const nextName = name.trim();
+
+ if (!nextName || nextName === layer.name) {
+ return false;
+ }
+
+ layer.name = nextName;
+ return true;
+ }
+
+ 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;
+ this.updateActiveLayerClass();
+
+ 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;
+ this.updateActiveLayerClass();
+ }
+ }
+ }
+
+ 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;
+ }
+
+ 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];
+
+ 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/src/core/layer-transforms.ts b/src/core/layer-transforms.ts
new file mode 100644
index 0000000..32ca1c1
--- /dev/null
+++ b/src/core/layer-transforms.ts
@@ -0,0 +1,92 @@
+import type {MirrorAxis, PixelBuffer, RotationDirection} from '../types';
+
+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: 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.height - 1 - y, y: x}
+ : {x: y, y: buffer.width - 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/src/core/platform.ts b/src/core/platform.ts
new file mode 100644
index 0000000..06850e2
--- /dev/null
+++ b/src/core/platform.ts
@@ -0,0 +1,4 @@
+export const isMac =
+ navigator.platform.startsWith('Mac') || navigator.userAgent.includes('Macintosh');
+
+export const modKey = isMac ? '⌘' : 'Ctrl';
diff --git a/src/core/project-file.ts b/src/core/project-file.ts
new file mode 100644
index 0000000..fa1763b
--- /dev/null
+++ b/src/core/project-file.ts
@@ -0,0 +1,230 @@
+import type {ParsedWdraftFile, TextLayerData, WdraftFileMeta, WdraftLayerMeta} from '../types';
+
+export type {WdraftLayerMeta, WdraftFileMeta, ParsedWdraftFile};
+
+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';
+
+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),
+ textDataJson: l.textData ? enc.encode(JSON.stringify(l.textData)) : null,
+ }));
+
+ let size =
+ 4 + // magic
+ 1 + // version
+ 4 + // canvasWidth
+ 4 + // canvasHeight
+ 4 + // layerCount
+ 2 + activeLayerIdBytes.length + // activeLayerId
+ 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
+ 2 + (td ? td.length : 0); // textData length + data (0 = none)
+ }
+
+ const buffer = new ArrayBuffer(size);
+ const view = new DataView(buffer);
+ const bytes = new Uint8Array(buffer);
+
+ let offset = 0;
+
+ for (const byte of WDRAFT_MAGIC) bytes[offset++] = byte;
+ bytes[offset++] = WDRAFT_VERSION;
+
+ view.setUint32(offset, meta.canvasWidth, true);
+ offset += 4;
+ view.setUint32(offset, meta.canvasHeight, true);
+ offset += 4;
+ view.setUint32(offset, meta.layerCount, true);
+ offset += 4;
+
+ view.setUint16(offset, activeLayerIdBytes.length, true);
+ offset += 2;
+ bytes.set(activeLayerIdBytes, offset);
+ offset += activeLayerIdBytes.length;
+
+ view.setUint16(offset, meta.layers.length, true);
+ offset += 2;
+
+ 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, 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;
+
+ // 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});
+}
+
+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);
+
+ if (buffer.byteLength < 5) {
+ throw new Error('Invalid .wdraft file: too short.');
+ }
+
+ for (let i = 0; i < WDRAFT_MAGIC.length; i++) {
+ if (bytes[i] !== WDRAFT_MAGIC[i]) {
+ throw new Error('Invalid .wdraft file: wrong magic bytes.');
+ }
+ }
+
+ let offset = 4;
+
+ const version = bytes[offset++];
+ if (version !== 2 && version !== WDRAFT_VERSION) {
+ throw new Error(`Unsupported .wdraft version: ${version}. Expected ${WDRAFT_VERSION}.`);
+ }
+
+ 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: 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;
+
+ let textData: TextLayerData | undefined;
+ if (version >= 3) {
+ const tdLen = view.getUint16(offset, true);
+ offset += 2;
+ if (tdLen > 0) {
+ try {
+ textData = migrateTextData(JSON.parse(dec.decode(bytes.subarray(offset, offset + tdLen))));
+ } catch {
+ // malformed textData — ignore
+ }
+ offset += tdLen;
+ }
+ }
+
+ layers.push({id, name, visible, width, height, textData});
+ pngBuffers.push(png);
+ }
+
+ return {
+ meta: {version, canvasWidth, canvasHeight, activeLayerId, layerCount, layers},
+ pngBuffers,
+ };
+}
diff --git a/src/core/rich-text-renderer.ts b/src/core/rich-text-renderer.ts
new file mode 100644
index 0000000..86a6101
--- /dev/null
+++ b/src/core/rich-text-renderer.ts
@@ -0,0 +1,204 @@
+import type {SizeWithPosition, TextLayerData, StyledRun} from '../types';
+
+export function drawRichText(
+ context: CanvasRenderingContext2D,
+ textData: TextLayerData,
+ drawBounds: SizeWithPosition,
+): void {
+ const htmlLines = parseHtmlToLines(textData.html, textData);
+
+ context.save();
+ context.globalCompositeOperation = 'source-over';
+ context.textBaseline = 'top';
+
+ let y = drawBounds.y;
+
+ 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();
+}
+
+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];
+ }
+}
+
+function runToFont(run: StyledRun): string {
+ return `${run.italic ? 'italic' : 'normal'} ${run.bold ? '700' : '400'} ${run.fontSize}px ${run.fontFamily}`;
+}
+
+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;
+}
diff --git a/src/core/selection-controller.ts b/src/core/selection-controller.ts
new file mode 100644
index 0000000..0821428
--- /dev/null
+++ b/src/core/selection-controller.ts
@@ -0,0 +1,127 @@
+import type {EditorServices, Point, SizeWithPosition, ClipboardSnapshot} from '../types';
+import {getBounds, getClippedPasteBounds, normalizeCanvasBounds} from './editor-geometry';
+
+export class SelectionController {
+ private startPoint: Point | null = null;
+ private _bounds: SizeWithPosition | null = null;
+ private clipboard: ClipboardSnapshot | null = null;
+
+ constructor(private readonly svc: EditorServices) {
+ }
+
+ get hasBounds(): boolean {
+ return this._bounds !== null;
+ }
+
+ get bounds(): SizeWithPosition | null {
+ return this._bounds;
+ }
+
+ get canPaste(): boolean {
+ return this.clipboard !== null;
+ }
+
+ onPointerDown(point: Point): void {
+ this.startPoint = point;
+ this._bounds = null;
+ this.svc.clearPreview();
+ }
+
+ onPointerMove(point: Point): void {
+ if (!this.startPoint) return;
+ this._bounds = getBounds(this.startPoint, point);
+ this.renderFrame();
+ }
+
+ onPointerUp(point: Point): void {
+ if (!this.startPoint) return;
+ this._bounds = getBounds(this.startPoint, point);
+ if (!normalizeCanvasBounds(this._bounds, this.canvasSize)) {
+ this.clear();
+ return;
+ }
+ this.renderFrame();
+ this.svc.dispatchChange();
+ }
+
+ copy(): void {
+ if (!this._bounds) return;
+ const bounds = normalizeCanvasBounds(this._bounds, this.canvasSize);
+ if (!bounds) return;
+ const {context} = this.svc.layerManager.activeLayer;
+ this.clipboard = {
+ bounds,
+ imageData: context.getImageData(bounds.x, bounds.y, bounds.width, bounds.height),
+ };
+ this.svc.dispatchChange();
+ }
+
+ cut(): void {
+ if (!this._bounds) return;
+ const bounds = normalizeCanvasBounds(this._bounds, this.canvasSize);
+ if (!bounds) return;
+ const before = this.svc.captureSnapshot();
+ const {context} = this.svc.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.svc.pushHistory(before, this.svc.captureSnapshot());
+ this.svc.dispatchChange();
+ }
+
+ paste(): void {
+ if (!this.clipboard) return;
+ const before = this.svc.captureSnapshot();
+ const {context} = this.svc.layerManager.activeLayer;
+ const target = this._bounds ?? this.clipboard.bounds;
+ const pasteBounds = getClippedPasteBounds(target, this.clipboard.bounds, this.canvasSize);
+ 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._bounds = {
+ x: pasteBounds.targetX,
+ y: pasteBounds.targetY,
+ width: this.clipboard.bounds.width,
+ height: this.clipboard.bounds.height,
+ };
+ this.renderFrame();
+ this.svc.pushHistory(before, this.svc.captureSnapshot());
+ this.svc.dispatchChange();
+ }
+
+ clear(): void {
+ this.startPoint = null;
+ this._bounds = null;
+ this.svc.clearPreview();
+ }
+
+ private get canvasSize(): { width: number; height: number } {
+ return {width: this.svc.state.canvasWidth, height: this.svc.state.canvasHeight};
+ }
+
+ private renderFrame(): void {
+ if (!this._bounds) return;
+ const bounds = normalizeCanvasBounds(this._bounds, this.canvasSize);
+ if (!bounds) {
+ this.svc.clearPreview();
+ return;
+ }
+ this.svc.clearPreview();
+ const ctx = this.svc.previewContext;
+ ctx.save();
+ ctx.setLineDash([6, 4]);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = '#1b6cff';
+ ctx.strokeRect(bounds.x + 0.5, bounds.y + 0.5, bounds.width, bounds.height);
+ ctx.restore();
+ }
+}
diff --git a/src/core/text-edit-controller.ts b/src/core/text-edit-controller.ts
new file mode 100644
index 0000000..9a1d682
--- /dev/null
+++ b/src/core/text-edit-controller.ts
@@ -0,0 +1,279 @@
+import type {EditorServices, Layer, Point, SizeWithPosition, TextToolbarDefaults} from '../types';
+import {createTextToolbar} from '../ui/text-toolbar';
+import {getBounds, normalizeTextBounds} from './editor-geometry';
+import {drawRichText} from './rich-text-renderer';
+
+export class TextEditController {
+ private startPoint: Point | null = null;
+ private _bounds: SizeWithPosition | null = null;
+ private inputEl: HTMLElement | null = null;
+ private toolbarEl: HTMLElement | null = null;
+ private toolbarCleanup: (() => void) | null = null;
+ private inputDefaults: TextToolbarDefaults | null = null;
+ private skipNextPointerDown = false;
+ private pendingLayerEdit: Layer | null = null;
+ private editingLayer: Layer | null = null;
+ private editSnapshot: ImageData | null = null;
+
+ constructor(private readonly svc: EditorServices) {
+ }
+
+ get isActive(): boolean {
+ return this.inputEl !== null;
+ }
+
+ /**
+ * Returns true if the pointer down was fully handled (caller should not start a draw stroke).
+ */
+ onPointerDown(point: Point): boolean {
+ if (this.inputEl) {
+ this.commit();
+ return true;
+ }
+
+ const textLayerAtPoint = this.findTextLayerAt(point);
+
+ if (this.skipNextPointerDown) {
+ this.skipNextPointerDown = false;
+ if (!textLayerAtPoint) return true;
+ }
+
+ this.commit();
+
+ if (textLayerAtPoint) {
+ this.pendingLayerEdit = textLayerAtPoint;
+ return true;
+ }
+
+ this.startPoint = point;
+ this._bounds = null;
+ this.svc.clearPreview();
+ return false;
+ }
+
+ onPointerMove(point: Point): void {
+ if (!this.startPoint) return;
+ this._bounds = getBounds(this.startPoint, point);
+ this.renderFrame();
+ }
+
+ onPointerUp(point: Point): void {
+ if (this.pendingLayerEdit) {
+ this.enterEditMode(this.pendingLayerEdit);
+ this.pendingLayerEdit = null;
+ return;
+ }
+ if (!this.startPoint) return;
+ const bounds = normalizeTextBounds(
+ getBounds(this.startPoint, point),
+ this.canvasSize,
+ );
+ this._bounds = bounds;
+ this.svc.clearPreview();
+ this.createInput(bounds, {
+ fontSize: 16,
+ fontFamily: 'sans-serif',
+ color: this.svc.state.color,
+ align: 'left',
+ });
+ this.startPoint = null;
+ }
+
+ commit(options: { skipNextPointerDown?: boolean } = {}): void {
+ if (!this.inputEl || !this._bounds) return;
+
+ const html = this.inputEl.innerHTML;
+ const bounds = this._bounds;
+ const editingLayer = this.editingLayer;
+ const snapshot = this.editSnapshot;
+ const defaults = this.inputDefaults ?? {
+ fontSize: 16,
+ fontFamily: 'sans-serif',
+ color: '#000000',
+ align: 'left' as CanvasTextAlign,
+ };
+
+ this.removeInput();
+ this.svc.clearPreview();
+ this.skipNextPointerDown = options.skipNextPointerDown ?? false;
+ this._bounds = null;
+ this.editingLayer = null;
+ this.editSnapshot = null;
+
+ 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.svc.captureSnapshot();
+
+ if (editingLayer) {
+ 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.svc.layerManager.createLayer(
+ this.svc.state.canvasWidth,
+ this.svc.state.canvasHeight,
+ );
+ drawRichText(newLayer.context, textData, bounds);
+ newLayer.textData = textData;
+ newLayer.name = `T: ${plainText.slice(0, 18)}`;
+ }
+
+ this.svc.pushHistory(before, this.svc.captureSnapshot());
+ this.svc.dispatchChange();
+ }
+
+ private enterEditMode(layer: Layer): void {
+ if (!layer.textData) return;
+ if (layer.id !== this.svc.layerManager.activeLayer.id) {
+ this.svc.layerManager.selectLayer(layer.id);
+ this.svc.dispatchChange();
+ }
+ this.editSnapshot = layer.context.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
+ layer.context.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
+ this._bounds = layer.textData.bounds;
+ this.editingLayer = layer;
+ this.createInput(layer.textData.bounds, {
+ fontSize: layer.textData.defaultFontSize,
+ fontFamily: layer.textData.defaultFontFamily,
+ color: layer.textData.defaultColor,
+ align: layer.textData.defaultAlign,
+ });
+ if (this.inputEl) {
+ this.inputEl.innerHTML = layer.textData.html;
+ }
+ }
+
+ private findTextLayerAt(point: Point): Layer | null {
+ const layers = this.svc.layerManager.allLayers;
+ 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;
+ }
+ }
+ 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 createInput(bounds: SizeWithPosition, defaults: TextToolbarDefaults): void {
+ this.removeInput();
+
+ 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 = 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.commit();
+ }
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ const cancelLayer = this.editingLayer;
+ const cancelSnapshot = this.editSnapshot;
+ this.editingLayer = null;
+ this.editSnapshot = null;
+ this._bounds = null;
+ if (cancelLayer && cancelSnapshot) {
+ cancelLayer.context.putImageData(cancelSnapshot, 0, 0);
+ }
+ this.removeInput();
+ this.svc.clearPreview();
+ }
+ });
+
+ input.addEventListener('blur', (event) => {
+ const related = (event as FocusEvent).relatedTarget as Node | null;
+ if (this.toolbarEl?.contains(related)) return;
+ this.commit({skipNextPointerDown: true});
+ });
+
+ this.inputEl = input;
+ this.inputDefaults = defaults;
+
+ const {element: toolbar, cleanup} = createTextToolbar(input, bounds, defaults, (align) => {
+ if (this.inputDefaults) this.inputDefaults.align = align;
+ });
+ this.toolbarEl = toolbar;
+ this.toolbarCleanup = cleanup;
+
+ this.svc.root.append(input);
+ this.svc.root.append(toolbar);
+ input.focus();
+ }
+
+ private removeInput(): void {
+ this.toolbarCleanup?.();
+ this.toolbarCleanup = null;
+ this.toolbarEl?.remove();
+ this.toolbarEl = null;
+ this.inputEl?.remove();
+ this.inputEl = null;
+ this.inputDefaults = null;
+ }
+
+ private renderFrame(): void {
+ if (!this._bounds) return;
+ const bounds = normalizeTextBounds(this._bounds, this.canvasSize);
+ this.svc.clearPreview();
+ const ctx = this.svc.previewContext;
+ ctx.save();
+ ctx.setLineDash([4, 4]);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = '#6b9dff';
+ ctx.strokeRect(bounds.x + 0.5, bounds.y + 0.5, bounds.width, bounds.height);
+ ctx.restore();
+ }
+
+ private get canvasSize(): { width: number; height: number } {
+ return {width: this.svc.state.canvasWidth, height: this.svc.state.canvasHeight};
+ }
+}
+
+function htmlToPlainText(html: string): string {
+ const tmp = document.createElement('div');
+ tmp.innerHTML = html;
+ return tmp.textContent ?? '';
+}
diff --git a/src/core/webdraft-editor.ts b/src/core/webdraft-editor.ts
new file mode 100644
index 0000000..83046cc
--- /dev/null
+++ b/src/core/webdraft-editor.ts
@@ -0,0 +1,864 @@
+import type {
+ EditorOptions,
+ EditorServices,
+ EditorState,
+ HistorySnapshot,
+ LayerSummary,
+ ParsedWdraftFile,
+ Point,
+ WdraftFileMeta,
+} from '../types';
+import {Tool} from '../types';
+import {drawLine, drawPoint, drawShape, drawWebLine} from './canvas-drawing';
+import {fitNaturalSizeToCanvas, getBounds} 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';
+import {serializeWdraftBinary, WDRAFT_VERSION} from './project-file';
+import {SelectionController} from './selection-controller';
+import {TextEditController} from './text-edit-controller';
+
+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;
+ private readonly previewCanvas: HTMLCanvasElement;
+ private readonly previewContext: CanvasRenderingContext2D;
+ private readonly eventLayer: HTMLDivElement;
+ private readonly selection: SelectionController;
+ private readonly text: TextEditController;
+ private isDrawing = false;
+ private lastPoint: Point | null = null;
+ private shapeStartPoint: Point | null = null;
+ private webPoints: Point[] = [];
+ private pendingHistorySnapshot: HistorySnapshot | null = null;
+ private readonly history = new HistoryManager(30);
+
+ readonly state: EditorState;
+
+ constructor(root: HTMLElement, options: EditorOptions) {
+ super();
+
+ this.root = root;
+ 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,
+ zoom: 1,
+ color: options.color,
+ fillColor: '#ffffff',
+ fillEnabled: false,
+ fillOpacity: 100,
+ fillTolerance: 0,
+ size: options.size,
+ canvasWidth: options.width,
+ canvasHeight: options.height,
+ webSensitivity: 100,
+ shadowEnabled: false,
+ shadowColor: '#000000',
+ shadowBlur: 8,
+ shadowOffsetX: 8,
+ shadowOffsetY: 8,
+ };
+
+ const services: EditorServices = {
+ state: this.state,
+ layerManager: this.layerManager,
+ previewContext: this.previewContext,
+ previewCanvas: this.previewCanvas,
+ root: this.root,
+ captureSnapshot: () => this.captureHistorySnapshot(),
+ pushHistory: (b, a) => this.pushHistory(b, a),
+ clearPreview: () => this.clearPreview(),
+ dispatchChange: () => this.dispatchChange(),
+ };
+
+ this.selection = new SelectionController(services);
+ this.text = new TextEditController(services);
+ }
+
+ mount(): void {
+ this.applyCanvasSize();
+ this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight);
+ this.root.append(this.previewCanvas);
+ this.root.append(this.eventLayer);
+ this.bindPointerEvents();
+ this.dispatchChange();
+ }
+
+ get layers(): LayerSummary[] {
+ return this.layerManager.summaries;
+ }
+
+ get canUndo(): boolean {
+ return this.history.canUndo;
+ }
+
+ get canRedo(): boolean {
+ return this.history.canRedo;
+ }
+
+ get hasSelection(): boolean {
+ return this.selection.hasBounds;
+ }
+
+ get canPaste(): boolean {
+ return this.selection.canPaste;
+ }
+
+ 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.text.commit();
+ this.state.activeTool = tool;
+ if (tool !== Tool.Select) {
+ this.selection.clear();
+ }
+ this.dispatchChange();
+ }
+
+ setColor(color: string): void {
+ this.state.color = color;
+ this.dispatchChange();
+ }
+
+ setFillColor(color: string): void {
+ this.state.fillColor = color;
+ this.dispatchChange();
+ }
+
+ setFillEnabled(enabled: boolean): void {
+ this.state.fillEnabled = enabled;
+ this.dispatchChange();
+ }
+
+ setFillOpacity(opacity: number): void {
+ this.state.fillOpacity = Math.min(Math.max(opacity, 0), 100);
+ 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();
+ }
+
+ setWebSensitivity(sensitivity: number): void {
+ this.state.webSensitivity = Math.min(Math.max(sensitivity, 20), 260);
+ 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();
+ }
+
+ 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.text.commit();
+ this.selection.clear();
+
+ 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;
+ }
+
+ const before = this.captureHistorySnapshot();
+ this.state.canvasWidth = nextWidth;
+ this.state.canvasHeight = nextHeight;
+ if (resizeLayersToo) this.layerManager.resizeLayers(nextWidth, nextHeight);
+ this.applyCanvasSize();
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ resizeActiveLayer(width: number, height: number): void {
+ this.text.commit();
+ this.selection.clear();
+
+ 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();
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ undo(): void {
+ const snapshot = this.history.undo();
+ if (!snapshot) return;
+ this.restoreHistorySnapshot(snapshot);
+ this.dispatchChange();
+ }
+
+ redo(): void {
+ const snapshot = this.history.redo();
+ if (!snapshot) return;
+ this.restoreHistorySnapshot(snapshot);
+ 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.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ 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 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.pushHistory(before, this.captureHistorySnapshot());
+ } finally {
+ stream.getTracks().forEach((track) => track.stop());
+ }
+ }
+
+ 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 exportProject(): Promise {
+ const snapshot = this.layerManager.captureDocument();
+ 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: snapshot.layers.map((l) => ({
+ id: l.layerId,
+ name: l.name,
+ visible: l.visible,
+ width: l.imageData.width,
+ height: l.imageData.height,
+ textData: l.textData,
+ })),
+ };
+ return serializeWdraftBinary(meta, pngBuffers);
+ }
+
+ async importProject(parsed: ParsedWdraftFile): Promise {
+ const {meta, pngBuffers} = parsed;
+ const before = this.captureHistorySnapshot();
+ const imageDataList = await Promise.all(
+ meta.layers.map((l, i) => loadPngBytesAsImageData(pngBuffers[i], l.width, l.height)),
+ );
+ const newDocument = {
+ activeLayerId: meta.activeLayerId,
+ layerCount: meta.layerCount,
+ layers: meta.layers.map((l, i) => ({
+ layerId: l.id,
+ name: l.name,
+ visible: l.visible,
+ x: 0,
+ y: 0,
+ textData: l.textData,
+ imageData: imageDataList[i],
+ })),
+ };
+ this.state.canvasWidth = meta.canvasWidth;
+ this.state.canvasHeight = meta.canvasHeight;
+ this.layerManager.restoreDocument(newDocument);
+ this.applyCanvasSize();
+ this.selection.clear();
+ this.pushHistory(before, this.captureHistorySnapshot());
+ 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.state.canvasWidth;
+ canvas.height = this.state.canvasHeight;
+
+ for (const layer of this.layerManager.visibleLayers) {
+ context.drawImage(layer.canvas, layer.x, layer.y);
+ }
+
+ const blob = await new Promise((resolve) => {
+ canvas.toBlob(resolve, 'image/png');
+ });
+
+ if (!blob) {
+ throw new Error('Unable to export PNG.');
+ }
+
+ return blob;
+ }
+
+ addLayer(): void {
+ const before = this.captureHistorySnapshot();
+ this.layerManager.createLayer(this.state.canvasWidth, this.state.canvasHeight);
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ deleteActiveLayer(): void {
+ const before = this.captureHistorySnapshot();
+ if (this.layerManager.deleteActiveLayer()) {
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+ }
+
+ selectLayer(id: string): void {
+ this.layerManager.selectLayer(id);
+ this.dispatchChange();
+ }
+
+ renameLayer(id: string, name: string): void {
+ 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.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ moveActiveLayerUp(): void {
+ const before = this.captureHistorySnapshot();
+ if (this.layerManager.moveActiveLayerUp()) {
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+ }
+
+ moveActiveLayerDown(): void {
+ const before = this.captureHistorySnapshot();
+ if (this.layerManager.moveActiveLayerDown()) {
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+ }
+
+ copySelection(): void {
+ this.selection.copy();
+ }
+
+ cutSelection(): void {
+ this.selection.cut();
+ }
+
+ pasteSelection(): void {
+ this.selection.paste();
+ }
+
+ invertActiveLayer(): void {
+ this.text.commit();
+ this.selection.clear();
+
+ 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.captureHistorySnapshot());
+ }
+
+ rotateActiveLayer(direction: 'left' | 'right'): void {
+ this.text.commit();
+ this.selection.clear();
+
+ const before = this.captureHistorySnapshot();
+ 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());
+ }
+
+ mirrorActiveLayer(axis: 'horizontal' | 'vertical'): void {
+ this.text.commit();
+ this.selection.clear();
+
+ 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.captureHistorySnapshot());
+ }
+
+ 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.selection.onPointerDown(this.lastPoint);
+ return;
+ }
+
+ if (this.state.activeTool === Tool.Text) {
+ const consumed = this.text.onPointerDown(this.lastPoint);
+ if (consumed) {
+ this.isDrawing = false;
+ this.lastPoint = null;
+ }
+ return;
+ }
+
+ if (this.state.activeTool === Tool.Sampler) {
+ this.sampleColor(this.lastPoint);
+ this.setTool(Tool.Pencil);
+ this.isDrawing = false;
+ this.lastPoint = null;
+ return;
+ }
+
+ if (this.state.activeTool === Tool.FillBucket) {
+ this.fillActiveLayer(this.toLayerPoint(this.lastPoint));
+ this.isDrawing = false;
+ this.lastPoint = null;
+ return;
+ }
+
+ this.pendingHistorySnapshot = this.captureHistorySnapshot();
+
+ if (this.isShapeTool()) {
+ this.shapeStartPoint = this.lastPoint;
+ return;
+ }
+
+ if (this.state.activeTool === Tool.Web) {
+ const lp = this.toLayerPoint(this.lastPoint);
+ this.webPoints = [lp];
+ drawPoint(this.layerManager.activeLayer.context, lp, this.state);
+ return;
+ }
+
+ drawPoint(this.layerManager.activeLayer.context, this.toLayerPoint(this.lastPoint), this.state);
+ });
+
+ this.eventLayer.addEventListener('pointermove', (event) => {
+ if (!this.isDrawing || !this.lastPoint) return;
+
+ const nextPoint = this.getPoint(event);
+
+ if (this.state.activeTool === Tool.Select) {
+ this.selection.onPointerMove(nextPoint);
+ return;
+ }
+
+ if (this.state.activeTool === Tool.Text) {
+ this.text.onPointerMove(nextPoint);
+ return;
+ }
+
+ if (this.isShapeTool()) {
+ this.renderShapePreview(nextPoint);
+ return;
+ }
+
+ if (this.state.activeTool === Tool.Web) {
+ this.webPoints = drawWebLine(
+ this.layerManager.activeLayer.context,
+ this.toLayerPoint(nextPoint),
+ this.webPoints,
+ this.state,
+ );
+ this.lastPoint = nextPoint;
+ return;
+ }
+
+ drawLine(
+ this.layerManager.activeLayer.context,
+ this.toLayerPoint(this.lastPoint),
+ this.toLayerPoint(nextPoint),
+ this.state,
+ );
+ this.lastPoint = nextPoint;
+ });
+
+ this.eventLayer.addEventListener('pointerup', (event) => {
+ const point = this.getPoint(event);
+
+ if (this.state.activeTool === Tool.Select) {
+ this.selection.onPointerUp(point);
+ }
+
+ if (this.state.activeTool === Tool.Text) {
+ this.text.onPointerUp(point);
+ }
+
+ if (this.isShapeTool()) {
+ this.commitShape(point);
+ }
+
+ if (this.state.activeTool !== Tool.Select && this.state.activeTool !== Tool.Text) {
+ this.commitPendingHistory();
+ }
+
+ this.eventLayer.releasePointerCapture(event.pointerId);
+ this.isDrawing = false;
+ this.lastPoint = null;
+ this.shapeStartPoint = null;
+ this.webPoints = [];
+ });
+
+ this.eventLayer.addEventListener('pointercancel', () => {
+ this.isDrawing = false;
+ this.lastPoint = null;
+ this.shapeStartPoint = null;
+ this.webPoints = [];
+ 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 {
+ 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();
+ const zoom = this.state.zoom;
+ return {
+ x: (event.clientX - rect.left) / zoom,
+ y: (event.clientY - rect.top) / zoom,
+ };
+ }
+
+ 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 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;
+ 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';
+ 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 } {
+ return {width: this.state.canvasWidth, height: this.state.canvasHeight};
+ }
+
+ private isShapeTool(): boolean {
+ 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.state.canvasWidth || y >= this.state.canvasHeight) {
+ return;
+ }
+
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+
+ if (!context) throw new Error('Canvas 2D context is unavailable.');
+
+ canvas.width = this.state.canvasWidth;
+ canvas.height = this.state.canvasHeight;
+
+ for (const layer of this.layerManager.visibleLayers) {
+ context.drawImage(layer.canvas, layer.x, layer.y);
+ }
+
+ const [red, green, blue, alpha] = context.getImageData(x, y, 1, 1).data;
+ if (alpha === 0) return;
+ 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),
+ this.state.fillTolerance,
+ );
+ if (!changed) return;
+ context.putImageData(imageData, 0, 0);
+ this.pushHistory(before, this.captureHistorySnapshot());
+ }
+
+ private renderShapePreview(point: Point): void {
+ if (!this.shapeStartPoint) return;
+ this.clearPreview();
+ drawShape(this.previewContext, getBounds(this.shapeStartPoint, point), this.state);
+ }
+
+ private commitShape(point: Point): void {
+ if (!this.shapeStartPoint) return;
+ const {context} = this.layerManager.activeLayer;
+ drawShape(
+ context,
+ getBounds(this.toLayerPoint(this.shapeStartPoint), this.toLayerPoint(point)),
+ this.state,
+ );
+ this.clearPreview();
+ }
+
+ private clearPreview(): void {
+ this.previewContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
+ }
+
+ private commitPendingHistory(): void {
+ if (!this.pendingHistorySnapshot) return;
+ this.pushHistory(this.pendingHistorySnapshot, this.captureHistorySnapshot());
+ this.pendingHistorySnapshot = null;
+ }
+
+ 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.selection.clear();
+ }
+
+ private pushHistory(before: HistorySnapshot, after: HistorySnapshot): void {
+ this.history.push(before, after);
+ this.dispatchChange();
+ }
+
+ private dispatchChange(): void {
+ this.dispatchEvent(new CustomEvent('change', {detail: this.state}));
+ }
+}
+
+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');
+}
+
+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);
+}
+
+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;
+ const ctx = canvas.getContext('2d')!;
+ ctx.drawImage(img, 0, 0);
+ resolve(ctx.getImageData(0, 0, width, height));
+ };
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ reject(new Error('Failed to load layer image.'));
+ };
+ img.src = url;
+ });
+}
diff --git a/src/i18n/en/camera.ts b/src/i18n/en/camera.ts
new file mode 100644
index 0000000..d792010
--- /dev/null
+++ b/src/i18n/en/camera.ts
@@ -0,0 +1,19 @@
+import type {CameraTranslations} from '../../types/i18n';
+
+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/src/i18n/en/common.ts b/src/i18n/en/common.ts
new file mode 100644
index 0000000..9eb95d5
--- /dev/null
+++ b/src/i18n/en/common.ts
@@ -0,0 +1,6 @@
+import type {CommonTranslations} from '../../types/i18n';
+
+export const commonTranslations: CommonTranslations = {
+ somethingWentWrong: 'Something went wrong.',
+ appTitle: 'WebDraft',
+};
diff --git a/src/i18n/en/edit.ts b/src/i18n/en/edit.ts
new file mode 100644
index 0000000..482f522
--- /dev/null
+++ b/src/i18n/en/edit.ts
@@ -0,0 +1,10 @@
+import type {EditTranslations} from '../../types/i18n';
+
+export const editTranslations: EditTranslations = {
+ undo: 'Undo',
+ redo: 'Redo',
+ cut: 'Cut',
+ copy: 'Copy',
+ paste: 'Paste',
+ clearLayer: 'Clear Layer',
+};
diff --git a/src/i18n/en/file.ts b/src/i18n/en/file.ts
new file mode 100644
index 0000000..af1e690
--- /dev/null
+++ b/src/i18n/en/file.ts
@@ -0,0 +1,16 @@
+import type {FileTranslations} from '../../types/i18n';
+
+export const fileTranslations: FileTranslations = {
+ new: 'New Project',
+ open: 'Open\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/src/i18n/en/help.ts b/src/i18n/en/help.ts
new file mode 100644
index 0000000..29b2371
--- /dev/null
+++ b/src/i18n/en/help.ts
@@ -0,0 +1,14 @@
+import type {HelpTranslations} from '../../types/i18n';
+
+export const helpTranslations: HelpTranslations = {
+ keyboardShortcuts: 'Keyboard Shortcuts',
+ close: 'Close',
+ sectionTools: 'Tools',
+ sectionView: 'View',
+ sectionCanvas: 'Canvas',
+ sizeDecrease: 'Decrease brush size',
+ sizeIncrease: 'Increase brush size',
+ zoomIn: 'Zoom in',
+ zoomOut: 'Zoom out',
+ zoomReset: 'Reset zoom',
+};
diff --git a/src/i18n/en/image.ts b/src/i18n/en/image.ts
new file mode 100644
index 0000000..a75e568
--- /dev/null
+++ b/src/i18n/en/image.ts
@@ -0,0 +1,14 @@
+import type {ImageTranslations} from '../../types/i18n';
+
+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}`,
+ applyToAllLayers: 'Apply to all layers',
+};
diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts
new file mode 100644
index 0000000..f9c7c23
--- /dev/null
+++ b/src/i18n/en/index.ts
@@ -0,0 +1,28 @@
+import type {Translations} from '../../types/i18n';
+import {cameraTranslations} from './camera.ts';
+import {commonTranslations} from './common.ts';
+import {editTranslations} from './edit.ts';
+import {fileTranslations} from './file.ts';
+import {helpTranslations} from './help.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 {saveDialogTranslations} from './saveDialog.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,
+ saveDialog: saveDialogTranslations,
+ help: helpTranslations,
+};
diff --git a/src/i18n/en/layer.ts b/src/i18n/en/layer.ts
new file mode 100644
index 0000000..14e5ade
--- /dev/null
+++ b/src/i18n/en/layer.ts
@@ -0,0 +1,12 @@
+import type {LayerTranslations} from '../../types/i18n';
+
+export const layerTranslations: LayerTranslations = {
+ new: 'New Layer',
+ delete: 'Delete Layer',
+ moveUp: 'Move Layer Up',
+ moveDown: 'Move Layer Down',
+ addedOk: 'Layer added.',
+ deletedOk: 'Layer deleted.',
+ resize: 'Resize Layer\u2026',
+ layerResizedOk: (w, h) => `Layer: ${w}\u00d7${h}`,
+};
diff --git a/src/i18n/en/layers.ts b/src/i18n/en/layers.ts
new file mode 100644
index 0000000..20024c2
--- /dev/null
+++ b/src/i18n/en/layers.ts
@@ -0,0 +1,15 @@
+import type {LayersTranslations} from '../../types/i18n';
+
+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}`,
+ renameLayer: 'Rename layer',
+ hideLayer: 'Hide layer',
+ showLayer: 'Show layer',
+ textLayer: 'Text layer',
+};
diff --git a/src/i18n/en/menu.ts b/src/i18n/en/menu.ts
new file mode 100644
index 0000000..44e78fc
--- /dev/null
+++ b/src/i18n/en/menu.ts
@@ -0,0 +1,9 @@
+import type {MenuTranslations} from '../../types/i18n';
+
+export const menuTranslations: MenuTranslations = {
+ file: 'File',
+ edit: 'Edit',
+ image: 'Image',
+ layer: 'Layer',
+ help: 'Help',
+};
diff --git a/src/i18n/en/picker.ts b/src/i18n/en/picker.ts
new file mode 100644
index 0000000..8829f7b
--- /dev/null
+++ b/src/i18n/en/picker.ts
@@ -0,0 +1,10 @@
+import type {PickerTranslations} from '../../types/i18n';
+
+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/src/i18n/en/saveDialog.ts b/src/i18n/en/saveDialog.ts
new file mode 100644
index 0000000..8589023
--- /dev/null
+++ b/src/i18n/en/saveDialog.ts
@@ -0,0 +1,9 @@
+import type {SaveDialogTranslations} from '../../types/i18n';
+
+export const saveDialogTranslations: SaveDialogTranslations = {
+ title: 'Save',
+ exportPng: 'Export PNG',
+ saveProject: 'Save project',
+ rememberChoice: 'Remember decision',
+ cancel: 'Cancel',
+};
diff --git a/src/i18n/en/toolbar.ts b/src/i18n/en/toolbar.ts
new file mode 100644
index 0000000..fabc461
--- /dev/null
+++ b/src/i18n/en/toolbar.ts
@@ -0,0 +1,52 @@
+import type {ToolbarTranslations} from '../../types/i18n';
+import {modKey} from '../../core/platform';
+
+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 ${modKey}+Z`,
+ redo: `Redo \u2014 ${modKey}+Shift+Z`,
+ copy: `Copy \u2014 ${modKey}+C`,
+ cut: `Cut \u2014 ${modKey}+X`,
+ paste: `Paste \u2014 ${modKey}+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/src/i18n/pl/camera.ts b/src/i18n/pl/camera.ts
new file mode 100644
index 0000000..d534625
--- /dev/null
+++ b/src/i18n/pl/camera.ts
@@ -0,0 +1,19 @@
+import type {CameraTranslations} from '../../types/i18n';
+
+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/src/i18n/pl/common.ts b/src/i18n/pl/common.ts
new file mode 100644
index 0000000..c808a2f
--- /dev/null
+++ b/src/i18n/pl/common.ts
@@ -0,0 +1,6 @@
+import type {CommonTranslations} from '../../types/i18n';
+
+export const commonTranslations: CommonTranslations = {
+ somethingWentWrong: 'Coś poszło nie tak.',
+ appTitle: 'WebDraft',
+};
diff --git a/src/i18n/pl/edit.ts b/src/i18n/pl/edit.ts
new file mode 100644
index 0000000..a37d64c
--- /dev/null
+++ b/src/i18n/pl/edit.ts
@@ -0,0 +1,10 @@
+import type {EditTranslations} from '../../types/i18n';
+
+export const editTranslations: EditTranslations = {
+ undo: 'Cofnij',
+ redo: 'Ponów',
+ cut: 'Wytnij',
+ copy: 'Kopiuj',
+ paste: 'Wklej',
+ clearLayer: 'Wyczyść warstwę',
+};
diff --git a/src/i18n/pl/file.ts b/src/i18n/pl/file.ts
new file mode 100644
index 0000000..946c7ee
--- /dev/null
+++ b/src/i18n/pl/file.ts
@@ -0,0 +1,16 @@
+import type {FileTranslations} from '../../types/i18n';
+
+export const fileTranslations: FileTranslations = {
+ new: 'Nowy projekt',
+ open: 'Otwórz\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/src/i18n/pl/help.ts b/src/i18n/pl/help.ts
new file mode 100644
index 0000000..733528d
--- /dev/null
+++ b/src/i18n/pl/help.ts
@@ -0,0 +1,14 @@
+import type {HelpTranslations} from '../../types/i18n';
+
+export const helpTranslations: HelpTranslations = {
+ keyboardShortcuts: 'Skróty klawiszowe',
+ close: 'Zamknij',
+ sectionTools: 'Narzędzia',
+ sectionView: 'Widok',
+ sectionCanvas: 'Płótno',
+ sizeDecrease: 'Zmniejsz rozmiar pędzla',
+ sizeIncrease: 'Zwiększ rozmiar pędzla',
+ zoomIn: 'Powiększ',
+ zoomOut: 'Pomniejsz',
+ zoomReset: 'Resetuj zoom',
+};
diff --git a/src/i18n/pl/image.ts b/src/i18n/pl/image.ts
new file mode 100644
index 0000000..6278d10
--- /dev/null
+++ b/src/i18n/pl/image.ts
@@ -0,0 +1,14 @@
+import type {ImageTranslations} from '../../types/i18n';
+
+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}`,
+ applyToAllLayers: 'Zastosuj do wszystkich warstw',
+};
diff --git a/src/i18n/pl/index.ts b/src/i18n/pl/index.ts
new file mode 100644
index 0000000..83b60cd
--- /dev/null
+++ b/src/i18n/pl/index.ts
@@ -0,0 +1,28 @@
+import type {Translations} from '../../types/i18n';
+import {cameraTranslations} from './camera.ts';
+import {commonTranslations} from './common.ts';
+import {editTranslations} from './edit.ts';
+import {fileTranslations} from './file.ts';
+import {helpTranslations} from './help.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 {saveDialogTranslations} from './saveDialog.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,
+ saveDialog: saveDialogTranslations,
+ help: helpTranslations,
+};
diff --git a/src/i18n/pl/layer.ts b/src/i18n/pl/layer.ts
new file mode 100644
index 0000000..0a5e46b
--- /dev/null
+++ b/src/i18n/pl/layer.ts
@@ -0,0 +1,12 @@
+import type {LayerTranslations} from '../../types/i18n';
+
+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.',
+ resize: 'Zmień rozmiar warstwy\u2026',
+ layerResizedOk: (w, h) => `Warstwa: ${w}\u00d7${h}`,
+};
diff --git a/src/i18n/pl/layers.ts b/src/i18n/pl/layers.ts
new file mode 100644
index 0000000..8d589bf
--- /dev/null
+++ b/src/i18n/pl/layers.ts
@@ -0,0 +1,15 @@
+import type {LayersTranslations} from '../../types/i18n';
+
+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}`,
+ renameLayer: 'Zmień nazwę warstwy',
+ hideLayer: 'Ukryj warstwę',
+ showLayer: 'Pokaż warstwę',
+ textLayer: 'Warstwa tekstowa',
+};
diff --git a/src/i18n/pl/menu.ts b/src/i18n/pl/menu.ts
new file mode 100644
index 0000000..cbdacff
--- /dev/null
+++ b/src/i18n/pl/menu.ts
@@ -0,0 +1,9 @@
+import type {MenuTranslations} from '../../types/i18n';
+
+export const menuTranslations: MenuTranslations = {
+ file: 'Plik',
+ edit: 'Edycja',
+ image: 'Obraz',
+ layer: 'Warstwa',
+ help: 'Pomoc',
+};
diff --git a/src/i18n/pl/picker.ts b/src/i18n/pl/picker.ts
new file mode 100644
index 0000000..04f8aef
--- /dev/null
+++ b/src/i18n/pl/picker.ts
@@ -0,0 +1,10 @@
+import type {PickerTranslations} from '../../types/i18n';
+
+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/src/i18n/pl/saveDialog.ts b/src/i18n/pl/saveDialog.ts
new file mode 100644
index 0000000..37ccf3b
--- /dev/null
+++ b/src/i18n/pl/saveDialog.ts
@@ -0,0 +1,9 @@
+import type {SaveDialogTranslations} from '../../types/i18n';
+
+export const saveDialogTranslations: SaveDialogTranslations = {
+ title: 'Zapisz',
+ exportPng: 'Eksportuj PNG',
+ saveProject: 'Zapisz projekt',
+ rememberChoice: 'Zapamiętaj decyzję',
+ cancel: 'Anuluj',
+};
diff --git a/src/i18n/pl/toolbar.ts b/src/i18n/pl/toolbar.ts
new file mode 100644
index 0000000..e5124e6
--- /dev/null
+++ b/src/i18n/pl/toolbar.ts
@@ -0,0 +1,52 @@
+import type {ToolbarTranslations} from '../../types/i18n';
+import {modKey} from '../../core/platform';
+
+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 ${modKey}+Z`,
+ redo: `Ponów \u2014 ${modKey}+Shift+Z`,
+ copy: `Kopiuj \u2014 ${modKey}+C`,
+ cut: `Wytnij \u2014 ${modKey}+X`,
+ paste: `Wklej \u2014 ${modKey}+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/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..b0ee3b0
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,51 @@
+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';
+import { restoreStateAfterReload } from './ui/lang-state';
+import './styles.css';
+
+const app = document.querySelector('#app');
+
+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';
+
+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';
+
+const rightPanel = document.createElement('aside');
+rightPanel.className = 'right-panel';
+
+const statusToasts = createStatusToasts();
+
+workspace.append(surface);
+shell.append(sidebar, workspace, rightPanel);
+appWrap.append(shell);
+app.append(appWrap, statusToasts);
+
+const editor = new WebDraftEditor(surface, {
+ width: 900,
+ height: 620,
+ color: '#111111',
+ size: 10
+});
+
+appWrap.prepend(createMenuBar(editor, statusToasts));
+sidebar.append(createToolbar(editor, statusToasts));
+rightPanel.append(createRightPanel(editor, statusToasts));
+editor.mount();
+void restoreStateAfterReload(editor);
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..daa1173
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,1625 @@
+:root {
+ color: #f4f7fb;
+ background: #202327;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+ overflow: hidden;
+ background: #202327;
+}
+
+button,
+input,
+select {
+ 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-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;
+ 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-lang-selector {
+ 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;
+}
+
+/* ── 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;
+ 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;
+ height: 100%;
+ max-height: 100%;
+ overflow: auto;
+ border-right: 1px solid #343941;
+ background: #17191d;
+}
+
+.right-panel {
+ min-width: 0;
+ height: 100%;
+ max-height: 100%;
+ overflow: auto;
+ border-left: 1px solid #343941;
+ background: #17191d;
+}
+
+.workspace {
+ display: grid;
+ place-items: center;
+ width: 100%;
+ min-width: 0;
+ height: 100%;
+ overflow: auto;
+ padding: 32px;
+}
+
+.canvas-surface {
+ position: relative;
+ flex-shrink: 0;
+ width: var(--canvas-width);
+ height: var(--canvas-height);
+ 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: visible;
+}
+
+.shape-preview-layer,
+.event-layer {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.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;
+}
+
+.event-layer {
+ z-index: 10;
+ touch-action: none;
+ cursor: crosshair;
+}
+
+.text-input-layer {
+ position: absolute;
+ z-index: 20;
+ min-width: 120px;
+ min-height: 40px;
+ padding: 6px;
+ 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 {
+ display: grid;
+ 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, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.tool-button,
+.command-button {
+ min-height: 40px;
+ border: 1px solid #343941;
+ border-radius: 6px;
+ color: #f4f7fb;
+ background: #242832;
+ cursor: pointer;
+}
+
+.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;
+}
+
+.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;
+}
+
+.color-picker,
+.checkbox-control,
+.range-control {
+ display: grid;
+ gap: 8px;
+ color: #c9d1dc;
+ 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;
+ cursor: pointer;
+ user-select: none;
+}
+
+.color-picker__swatch {
+ width: 24px;
+ 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 {
+ grid-template-columns: 1fr auto;
+}
+
+.range-control input {
+ grid-column: 1 / -1;
+ width: 100%;
+}
+
+.command-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ width: 100%;
+ padding: 0 12px;
+}
+
+.command-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.command-grid .command-button {
+ min-width: 0;
+ padding: 0 8px;
+ 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;
+ 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: 44px minmax(0, 1fr) 34px;
+ align-items: center;
+ min-height: 46px;
+ border: 1px solid #343941;
+ border-radius: 6px;
+ overflow: hidden;
+ background: #20242c;
+}
+
+.layer-item.is-active {
+ border-color: #5b9cff;
+ background: #17263c;
+}
+
+.layer-select,
+.layer-preview,
+.layer-name-input,
+.layer-visibility {
+ min-height: 46px;
+ border: 0;
+ color: #dce5f2;
+ background: transparent;
+ cursor: pointer;
+}
+
+.layer-preview {
+ position: relative;
+ 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-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;
+ 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 {
+ 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;
+ grid-template-rows: auto minmax(0, 1fr) auto;
+ min-height: 100svh;
+ }
+
+ .sidebar {
+ width: 100%;
+ min-height: 0;
+ max-height: none;
+ overflow: hidden;
+ border-right: 0;
+ 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;
+ 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(4, minmax(0, 1fr));
+ }
+
+ .workspace {
+ padding: 16px;
+ }
+
+ .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) {
+ .toolbar__section {
+ 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;
+}
+
+/* ── 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;
+}
+
+.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;
+}
+
+/* ── Keyboard shortcuts dialog ─────────────────────────────────────────── */
+
+.shortcuts-dialog-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgb(0 0 0 / 55%);
+}
+
+.shortcuts-dialog {
+ position: fixed;
+ z-index: 1001;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 560px;
+ max-height: 80vh;
+ overflow-y: auto;
+ background: #1e2126;
+ border: 1px solid #343941;
+ border-radius: 8px;
+ padding: 20px 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ box-shadow: 0 16px 48px rgb(0 0 0 / 55%);
+}
+
+.shortcuts-dialog__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.shortcuts-dialog__title {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: #f4f7fb;
+}
+
+.shortcuts-dialog__close {
+ background: none;
+ border: none;
+ color: #8a9099;
+ font-size: 20px;
+ line-height: 1;
+ cursor: pointer;
+ padding: 2px 7px;
+ border-radius: 4px;
+}
+
+.shortcuts-dialog__close:hover {
+ color: #f4f7fb;
+ background: #2c3038;
+}
+
+.shortcuts-dialog__body {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px 24px;
+}
+
+.shortcuts-section__title {
+ margin: 0 0 8px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: #5a6370;
+}
+
+.shortcuts-section__rows {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.shortcuts-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.shortcuts-row__key {
+ font-family: ui-monospace, monospace;
+ font-size: 11px;
+ background: #262b32;
+ border: 1px solid #3a4049;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 7px;
+ color: #c8cdd4;
+ white-space: nowrap;
+ min-width: 82px;
+ text-align: center;
+}
+
+.shortcuts-row__desc {
+ font-size: 12px;
+ color: #8a9099;
+}
diff --git a/src/types/camera.ts b/src/types/camera.ts
new file mode 100644
index 0000000..2ab4143
--- /dev/null
+++ b/src/types/camera.ts
@@ -0,0 +1,6 @@
+export type CameraFilters = {
+ sepia: boolean;
+ noise: boolean;
+ greyscale: boolean;
+ negative: boolean;
+};
diff --git a/src/types/clipboard.ts b/src/types/clipboard.ts
new file mode 100644
index 0000000..afdf36e
--- /dev/null
+++ b/src/types/clipboard.ts
@@ -0,0 +1,6 @@
+import {SizeWithPosition} from './core.ts';
+
+export type ClipboardSnapshot = {
+ bounds: SizeWithPosition;
+ imageData: ImageData;
+};
diff --git a/src/types/colors.ts b/src/types/colors.ts
new file mode 100644
index 0000000..5fc736f
--- /dev/null
+++ b/src/types/colors.ts
@@ -0,0 +1,6 @@
+export type RgbaColor = {
+ red: number;
+ green: number;
+ blue: number;
+ alpha: number;
+};
diff --git a/src/types/core.ts b/src/types/core.ts
new file mode 100644
index 0000000..4a69256
--- /dev/null
+++ b/src/types/core.ts
@@ -0,0 +1,58 @@
+export enum Tool {
+ Select = 'select',
+ Pencil = 'pencil',
+ Eraser = 'eraser',
+ Sampler = 'sampler',
+ FillBucket = 'fill-bucket',
+ Web = 'web',
+ Rectangle = 'rectangle',
+ Ellipse = 'ellipse',
+ Text = 'text',
+}
+
+export type Point = {
+ x: number;
+ y: number;
+};
+
+export type Size = {
+ width: number;
+ height: number;
+};
+
+export type SizeWithPosition = Size & Point;
+
+export type TextLayerData = {
+ html: string;
+ bounds: SizeWithPosition;
+ defaultFontSize: number;
+ defaultFontFamily: string;
+ defaultColor: string;
+ defaultAlign: CanvasTextAlign;
+};
+
+export type EditorOptions = {
+ width: number;
+ height: number;
+ color: string;
+ size: number;
+};
+
+export type EditorState = {
+ activeTool: Tool;
+ zoom: number;
+ color: string;
+ fillColor: string;
+ fillEnabled: boolean;
+ fillOpacity: number;
+ fillTolerance: number;
+ size: number;
+ canvasWidth: number;
+ canvasHeight: number;
+ webSensitivity: number;
+ shadowEnabled: boolean;
+ shadowColor: string;
+ shadowBlur: number;
+ shadowOffsetX: number;
+ shadowOffsetY: number;
+};
diff --git a/src/types/geometry.ts b/src/types/geometry.ts
new file mode 100644
index 0000000..d59cc5c
--- /dev/null
+++ b/src/types/geometry.ts
@@ -0,0 +1,13 @@
+export type NaturalSize = {
+ naturalWidth: number;
+ naturalHeight: number;
+};
+
+export type ClippedPasteBounds = {
+ targetX: number;
+ targetY: number;
+ sourceX: number;
+ sourceY: number;
+ width: number;
+ height: number;
+};
diff --git a/src/types/history.ts b/src/types/history.ts
new file mode 100644
index 0000000..d84891f
--- /dev/null
+++ b/src/types/history.ts
@@ -0,0 +1,26 @@
+import type {LayerManager} from '../core/layer-manager';
+import type {EditorState} from './core';
+import type {LayerDocumentSnapshot} from './layers';
+
+export type HistoryEntry = {
+ before: TSnapshot;
+ after: TSnapshot;
+};
+
+export type HistorySnapshot = {
+ canvasWidth: number;
+ canvasHeight: number;
+ document: LayerDocumentSnapshot;
+};
+
+export type EditorServices = {
+ readonly state: EditorState;
+ readonly layerManager: LayerManager;
+ readonly previewContext: CanvasRenderingContext2D;
+ readonly previewCanvas: HTMLCanvasElement;
+ readonly root: HTMLElement;
+ captureSnapshot(): HistorySnapshot;
+ pushHistory(before: HistorySnapshot, after: HistorySnapshot): void;
+ clearPreview(): void;
+ dispatchChange(): void;
+};
diff --git a/src/types/i18n.ts b/src/types/i18n.ts
new file mode 100644
index 0000000..c8751f9
--- /dev/null
+++ b/src/types/i18n.ts
@@ -0,0 +1,189 @@
+export type Lang = 'pl' | 'en';
+
+export type MenuTranslations = {
+ file: string;
+ edit: string;
+ image: string;
+ layer: string;
+ help: 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;
+ applyToAllLayers: string;
+};
+
+export type LayerTranslations = {
+ new: string;
+ delete: string;
+ moveUp: string;
+ moveDown: string;
+ addedOk: string;
+ deletedOk: string;
+ resize: string;
+ layerResizedOk: (w: number, h: number) => 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;
+ layerName: (suffix: string) => string;
+ moveLayerUp: string;
+ moveLayerDown: string;
+ selectLayer: (name: string) => string;
+ renameLayer: string;
+ hideLayer: string;
+ showLayer: string;
+ textLayer: 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 SaveDialogTranslations = {
+ title: string;
+ exportPng: string;
+ saveProject: string;
+ rememberChoice: string;
+ cancel: string;
+};
+
+export type HelpTranslations = {
+ keyboardShortcuts: string;
+ close: string;
+ sectionTools: string;
+ sectionView: string;
+ sectionCanvas: string;
+ sizeDecrease: string;
+ sizeIncrease: string;
+ zoomIn: string;
+ zoomOut: string;
+ zoomReset: string;
+};
+
+export type Translations = {
+ menu: MenuTranslations;
+ file: FileTranslations;
+ edit: EditTranslations;
+ image: ImageTranslations;
+ layer: LayerTranslations;
+ toolbar: ToolbarTranslations;
+ layers: LayersTranslations;
+ camera: CameraTranslations;
+ picker: PickerTranslations;
+ common: CommonTranslations;
+ saveDialog: SaveDialogTranslations;
+ help: HelpTranslations;
+};
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..b521050
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,13 @@
+export * from './core';
+export * from './layers';
+export * from './project';
+export * from './history';
+export * from './geometry';
+export * from './transforms';
+export * from './colors';
+export * from './ui';
+export * from './i18n';
+export * from './text';
+export * from './clipboard';
+export * from './camera';
+export * from './shortcuts';
diff --git a/src/types/layers.ts b/src/types/layers.ts
new file mode 100644
index 0000000..f37cdae
--- /dev/null
+++ b/src/types/layers.ts
@@ -0,0 +1,40 @@
+import type {TextLayerData} from './core';
+
+export type Layer = {
+ id: string;
+ name: string;
+ canvas: HTMLCanvasElement;
+ context: CanvasRenderingContext2D;
+ visible: boolean;
+ x: number;
+ y: number;
+ textData?: TextLayerData;
+};
+
+export type LayerSummary = {
+ id: string;
+ name: string;
+ active: boolean;
+ preview: string;
+ visible: boolean;
+ isTextLayer: boolean;
+};
+
+type LayerSnapshot = {
+ layerId: string;
+ imageData: ImageData;
+};
+
+type LayerSnapshotWithMetadata = LayerSnapshot & {
+ name: string;
+ visible: boolean;
+ x: number;
+ y: number;
+ textData?: TextLayerData;
+};
+
+export type LayerDocumentSnapshot = {
+ activeLayerId: string;
+ layerCount: number;
+ layers: LayerSnapshotWithMetadata[];
+};
diff --git a/src/types/project.ts b/src/types/project.ts
new file mode 100644
index 0000000..23951d5
--- /dev/null
+++ b/src/types/project.ts
@@ -0,0 +1,24 @@
+import type {TextLayerData} from './core';
+
+export type WdraftLayerMeta = {
+ id: string;
+ name: string;
+ visible: boolean;
+ width: number;
+ height: number;
+ textData?: TextLayerData;
+};
+
+export type WdraftFileMeta = {
+ version: number;
+ canvasWidth: number;
+ canvasHeight: number;
+ activeLayerId: string;
+ layerCount: number;
+ layers: WdraftLayerMeta[];
+};
+
+export type ParsedWdraftFile = {
+ meta: WdraftFileMeta;
+ pngBuffers: Uint8Array[];
+};
diff --git a/src/types/shortcuts.ts b/src/types/shortcuts.ts
new file mode 100644
index 0000000..deac9d8
--- /dev/null
+++ b/src/types/shortcuts.ts
@@ -0,0 +1,6 @@
+export type ShortcutOptions = {
+ openImagePicker: () => void;
+ exportImage: () => Promise;
+ onSave: () => void;
+ openShortcutsDialog: () => void;
+};
diff --git a/src/types/text.ts b/src/types/text.ts
new file mode 100644
index 0000000..0302f0b
--- /dev/null
+++ b/src/types/text.ts
@@ -0,0 +1,9 @@
+export type StyledRun = {
+ text: string;
+ bold: boolean;
+ italic: boolean;
+ underline: boolean;
+ color: string;
+ fontSize: number;
+ fontFamily: string;
+};
diff --git a/src/types/toolbar.ts b/src/types/toolbar.ts
new file mode 100644
index 0000000..ab4416f
--- /dev/null
+++ b/src/types/toolbar.ts
@@ -0,0 +1,12 @@
+import {Tool} from './core.ts';
+
+export type ToolbarSection = {
+ element: HTMLElement;
+ sync: () => void;
+};
+
+export type ToolConfig = {
+ id: Tool;
+ label: string;
+ icon: string;
+};
diff --git a/src/types/transforms.ts b/src/types/transforms.ts
new file mode 100644
index 0000000..0fcc05e
--- /dev/null
+++ b/src/types/transforms.ts
@@ -0,0 +1,8 @@
+export type PixelBuffer = {
+ width: number;
+ height: number;
+ data: Uint8ClampedArray;
+};
+
+export type RotationDirection = 'left' | 'right';
+export type MirrorAxis = 'horizontal' | 'vertical';
diff --git a/src/types/ui.ts b/src/types/ui.ts
new file mode 100644
index 0000000..b87a01e
--- /dev/null
+++ b/src/types/ui.ts
@@ -0,0 +1,49 @@
+export type StatusTone = 'info' | 'success' | 'error';
+
+export type StatusReporter = {
+ show: (message: string, tone?: StatusTone) => void;
+};
+
+export type ColorPickerOptions = {
+ label: string;
+ value: string;
+ onChange: (color: string) => void;
+};
+
+export type ColorPickerControl = HTMLElement & {
+ setValue: (color: string) => void;
+};
+
+export type TextToolbarDefaults = {
+ fontSize: number;
+ fontFamily: string;
+ color: string;
+ align: CanvasTextAlign;
+};
+
+export type SaveAction = 'png' | 'project';
+
+export type CheckboxControl = {
+ element: HTMLLabelElement;
+ setChecked: (checked: boolean) => void;
+};
+
+export type RangeControl = {
+ element: HTMLLabelElement;
+ setValue: (value: number) => void;
+};
+
+export type MenuItem =
+ | { type: 'action'; label: string; action: () => void; shortcut?: string; disabled?: () => boolean }
+ | { type: 'separator' };
+
+export type Menu = { label: string; items: MenuItem[] };
+
+export type ResizeDialogOptions = {
+ title: string;
+ width: number;
+ height: number;
+ checkboxLabel?: string;
+ checkboxChecked?: boolean;
+ onConfirm: (w: number, h: number, all: boolean) => void;
+};
diff --git a/src/ui/camera-panel.ts b/src/ui/camera-panel.ts
new file mode 100644
index 0000000..aa99070
--- /dev/null
+++ b/src/ui/camera-panel.ts
@@ -0,0 +1,274 @@
+import {t} from '../core/i18n';
+import type {WebDraftEditor} from '../core/webdraft-editor.ts';
+import type {CameraFilters, StatusReporter} from '../types';
+
+function applyFilters(data: Uint8ClampedArray, filters: CameraFilters): 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(t.camera.unavailable, 'error');
+ return;
+ }
+
+ const filters: CameraFilters = {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 = t.camera.title;
+
+ 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 CameraFilters }> = [
+ {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) {
+ 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 = t.camera.snap;
+ snapBtn.addEventListener('click', doSnap);
+
+ const snapToCanvasBtn = document.createElement('button');
+ snapToCanvasBtn.type = 'button';
+ snapToCanvasBtn.className = 'command-button';
+ snapToCanvasBtn.textContent = t.camera.snapToCanvas;
+ snapToCanvasBtn.addEventListener('click', doSnapToCanvas);
+
+ const applyBtn = document.createElement('button');
+ applyBtn.type = 'button';
+ applyBtn.className = 'command-button';
+ applyBtn.textContent = t.camera.applyToCanvas;
+ applyBtn.addEventListener('click', doApply);
+
+ const saveBtn = document.createElement('button');
+ saveBtn.type = 'button';
+ saveBtn.className = 'command-button';
+ saveBtn.textContent = t.camera.saveToFile;
+ saveBtn.addEventListener('click', doSave);
+
+ const backBtn = document.createElement('button');
+ backBtn.type = 'button';
+ backBtn.className = 'command-button';
+ backBtn.textContent = t.camera.backToLive;
+ 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 = t.camera.titleWithLabel(track.label);
+ }
+
+ previewCanvas.width = video.videoWidth;
+ previewCanvas.height = video.videoHeight;
+
+ startLiveLoop();
+ } catch (error) {
+ closePanel();
+ status.show(error instanceof Error ? error.message : t.camera.cameraError, '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(t.camera.frameAdded, '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 = t.camera.snapFilename;
+ 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(t.camera.frameAdded, '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/src/ui/color-picker.ts b/src/ui/color-picker.ts
new file mode 100644
index 0000000..020c99d
--- /dev/null
+++ b/src/ui/color-picker.ts
@@ -0,0 +1,1013 @@
+import {t} from '../core/i18n';
+import type {ColorPickerControl, ColorPickerOptions} from '../types';
+
+// 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 {
+ 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 = currentHex;
+
+ const labelEl = document.createElement('span');
+ labelEl.textContent = options.label;
+
+ wrapper.append(swatch, labelEl);
+ wrapper.addEventListener('pointerdown', (e) => {
+ e.stopPropagation();
+ if (popup) { closePopup(); } else { openPopup(); }
+ });
+
+ // ------------------------------------------------------------------ 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(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);
+
+ 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 = t.picker.addColor;
+ 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 = t.picker.restorePrevious;
+ 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(t.picker.newColor, currentHex, false));
+ bar.append(make(t.picker.previousColor, 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) {
+ 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); }
diff --git a/src/ui/keyboard-shortcuts.ts b/src/ui/keyboard-shortcuts.ts
new file mode 100644
index 0000000..a1bfd38
--- /dev/null
+++ b/src/ui/keyboard-shortcuts.ts
@@ -0,0 +1,177 @@
+import {Tool} from '../types';
+import type {ShortcutOptions} from '../types';
+import type {WebDraftEditor} from '../core/webdraft-editor';
+
+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 === '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();
+ return;
+ }
+
+ if (isCommand && key === 's') {
+ event.preventDefault();
+ options.onSave();
+ return;
+ }
+
+ if (isCommand && key === 'h') {
+ event.preventDefault();
+ options.openShortcutsDialog();
+ 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();
+ 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();
+ return;
+ }
+
+ if (key === '[') {
+ event.preventDefault();
+ editor.setSize(editor.state.size - 1);
+ return;
+ }
+
+ if (key === ']') {
+ event.preventDefault();
+ editor.setSize(editor.state.size + 1);
+ 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) {
+ event.preventDefault();
+ editor.setTool(tool);
+ }
+ });
+}
+
+function getToolForKey(key: string): Tool | null {
+ switch (key) {
+ case 's':
+ return Tool.Select;
+ case 'p':
+ return Tool.Pencil;
+ case 'e':
+ return Tool.Eraser;
+ case 'c':
+ return Tool.Sampler;
+ case 'b':
+ return Tool.FillBucket;
+ case 'w':
+ return Tool.Web;
+ case 'r':
+ return Tool.Rectangle;
+ case 'o':
+ return Tool.Ellipse;
+ case 't':
+ return Tool.Text;
+ 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/src/ui/lang-state.ts b/src/ui/lang-state.ts
new file mode 100644
index 0000000..5ddb292
--- /dev/null
+++ b/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/src/ui/layers-panel.ts b/src/ui/layers-panel.ts
new file mode 100644
index 0000000..187d2ee
--- /dev/null
+++ b/src/ui/layers-panel.ts
@@ -0,0 +1,101 @@
+import { t } from '../core/i18n';
+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 = t.layers.title;
+
+ const actions = document.createElement('div');
+ actions.className = 'panel-actions';
+
+ 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);
+
+ 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 previewButton = document.createElement('button');
+ previewButton.type = 'button';
+ previewButton.className = 'layer-preview';
+ previewButton.title = t.layers.selectLayer(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 = t.layers.renameLayer;
+ 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';
+ visibilityButton.className = 'layer-visibility';
+ visibilityButton.title = layer.visible ? t.layers.hideLayer : t.layers.showLayer;
+ 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;
+ })
+ );
+ };
+
+ 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/src/ui/menu-bar.ts b/src/ui/menu-bar.ts
new file mode 100644
index 0000000..f2a3963
--- /dev/null
+++ b/src/ui/menu-bar.ts
@@ -0,0 +1,486 @@
+import { parseWdraftBinary } from '../core/project-file';
+import { lang, setLang, t } from '../core/i18n';
+import { modKey } from '../core/platform';
+import { openShortcutsDialog } from './shortcuts-dialog';
+import type { WebDraftEditor } from '../core/webdraft-editor';
+import {Lang, Menu, MenuItem} from '../types';
+import type { StatusReporter } from '../types';
+import { getErrorMessage } from './status-toasts';
+import { saveStateForReload } from './lang-state';
+import { openResizeDialog } from './resize-dialog';
+
+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;
+}
+
+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';
+ bar.setAttribute('aria-label', t.menu.file);
+
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = 'image/*,.wdraft';
+ fileInput.className = 'visually-hidden';
+ fileInput.addEventListener('change', () => {
+ const file = fileInput.files?.[0];
+ if (!file) return;
+ void (async () => {
+ try {
+ if (file.name.endsWith('.wdraft')) {
+ const buffer = await file.arrayBuffer();
+ const parsed = parseWdraftBinary(buffer);
+ await editor.importProject(parsed);
+ status.show(t.file.openedOk, 'success');
+ } else {
+ await editor.importImage(file);
+ status.show(t.file.importedOk, 'success');
+ }
+ } catch (err) {
+ status.show(getErrorMessage(err), 'error');
+ } finally {
+ fileInput.value = '';
+ }
+ })();
+ });
+
+ bar.append(fileInput);
+
+ const menus: Menu[] = [
+ {
+ label: t.menu.file,
+ items: [
+ {
+ type: 'action',
+ label: t.file.new,
+ shortcut: `${modKey}+N`,
+ action: () => {
+ if (confirm(t.file.newConfirm)) {
+ editor.clear();
+ status.show(t.file.new, 'success');
+ }
+ },
+ },
+ {
+ type: 'action',
+ label: t.file.open,
+ shortcut: `${modKey}+O`,
+ action: () => fileInput.click(),
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.file.save,
+ action: () => {
+ void (async () => {
+ try {
+ const blob = await editor.exportProject();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = t.file.projectFilename;
+ a.click();
+ URL.revokeObjectURL(url);
+ status.show(t.file.savedOk, 'success');
+ } catch (err) {
+ status.show(getErrorMessage(err), 'error');
+ }
+ })();
+ },
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.file.exportPng,
+ action: () => {
+ void (async () => {
+ try {
+ const blob = await editor.exportPng();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = t.file.imageFilename;
+ a.click();
+ URL.revokeObjectURL(url);
+ status.show(t.file.exportedOk, 'success');
+ } catch (err) {
+ status.show(getErrorMessage(err), 'error');
+ }
+ })();
+ },
+ },
+ ],
+ },
+ {
+ label: t.menu.edit,
+ items: [
+ {
+ type: 'action',
+ label: t.edit.undo,
+ shortcut: `${modKey}+Z`,
+ disabled: () => !editor.canUndo,
+ action: () => editor.undo(),
+ },
+ {
+ type: 'action',
+ label: t.edit.redo,
+ shortcut: `${modKey}+Y`,
+ disabled: () => !editor.canRedo,
+ action: () => editor.redo(),
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.edit.cut,
+ shortcut: `${modKey}+X`,
+ action: () => editor.cutSelection(),
+ },
+ {
+ type: 'action',
+ label: t.edit.copy,
+ shortcut: `${modKey}+C`,
+ action: () => editor.copySelection(),
+ },
+ {
+ type: 'action',
+ label: t.edit.paste,
+ shortcut: `${modKey}+V`,
+ action: () => editor.pasteSelection(),
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.edit.clearLayer,
+ action: () => editor.clear(),
+ },
+ ],
+ },
+ {
+ label: t.menu.image,
+ items: [
+ {
+ type: 'action',
+ label: t.image.resizeCanvas,
+ action: () => {
+ 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' },
+ {
+ type: 'action',
+ label: t.image.invertColors,
+ action: () => editor.invertActiveLayer(),
+ },
+ {
+ type: 'action',
+ label: t.image.mirrorH,
+ action: () => editor.mirrorActiveLayer('horizontal'),
+ },
+ {
+ type: 'action',
+ label: t.image.mirrorV,
+ action: () => editor.mirrorActiveLayer('vertical'),
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.image.rotateLeft,
+ action: () => editor.rotateActiveLayer('left'),
+ },
+ {
+ type: 'action',
+ label: t.image.rotateRight,
+ action: () => editor.rotateActiveLayer('right'),
+ },
+ ],
+ },
+ {
+ label: t.menu.layer,
+ items: [
+ {
+ type: 'action',
+ label: t.layer.new,
+ action: () => {
+ editor.addLayer();
+ status.show(t.layer.addedOk, 'success');
+ },
+ },
+ {
+ type: 'action',
+ label: t.layer.delete,
+ disabled: () => editor.layers.length <= 1,
+ action: () => {
+ editor.deleteActiveLayer();
+ status.show(t.layer.deletedOk, 'success');
+ },
+ },
+ { type: 'separator' },
+ {
+ type: 'action',
+ label: t.layer.moveUp,
+ action: () => editor.moveActiveLayerUp(),
+ },
+ {
+ type: 'action',
+ 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');
+ },
+ });
+ },
+ },
+ ],
+ },
+ {
+ label: t.menu.help,
+ items: [
+ {
+ type: 'action',
+ label: t.help.keyboardShortcuts,
+ shortcut: `${modKey}+H`,
+ action: openShortcutsDialog,
+ },
+ ],
+ },
+ ];
+
+ for (const menu of menus) {
+ bar.append(createMenuButton(menu));
+ }
+
+ bar.append(createZoomWidget(editor), createLangSelector(editor));
+
+ document.addEventListener('click', closeOpenMenu);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeOpenMenu();
+ });
+
+ 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 (${modKey}+−)`;
+ btnOut.addEventListener('click', () => editor.zoomOut());
+
+ const label = document.createElement('button');
+ label.type = 'button';
+ label.className = 'menu-zoom-label';
+ label.title = `Reset zoom (${modKey}+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 (${modKey}+=)`;
+ 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;
+}
diff --git a/src/ui/resize-dialog.ts b/src/ui/resize-dialog.ts
new file mode 100644
index 0000000..773a8e2
--- /dev/null
+++ b/src/ui/resize-dialog.ts
@@ -0,0 +1,99 @@
+import { t } from '../core/i18n';
+import {ResizeDialogOptions} from '../types';
+
+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();
+ });
+}
diff --git a/src/ui/right-panel.ts b/src/ui/right-panel.ts
new file mode 100644
index 0000000..aa2ceae
--- /dev/null
+++ b/src/ui/right-panel.ts
@@ -0,0 +1,25 @@
+import type {WebDraftEditor} from '../core/webdraft-editor';
+import {StatusReporter} from '../types';
+import {createLayersPanel} from './layers-panel';
+import {createEditSection, syncToolbarSections} from './toolbar-sections';
+import {createToolbarSection} from './toolbar-controls';
+
+export function createRightPanel(editor: WebDraftEditor, status: StatusReporter): 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/src/ui/save-dialog.ts b/src/ui/save-dialog.ts
new file mode 100644
index 0000000..c68172e
--- /dev/null
+++ b/src/ui/save-dialog.ts
@@ -0,0 +1,104 @@
+import {t} from '../core/i18n';
+import type {SaveAction} from '../types';
+
+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/src/ui/shortcuts-dialog.ts b/src/ui/shortcuts-dialog.ts
new file mode 100644
index 0000000..5417fc9
--- /dev/null
+++ b/src/ui/shortcuts-dialog.ts
@@ -0,0 +1,147 @@
+import {t} from '../core/i18n';
+import {modKey} from '../core/platform';
+
+type ShortcutRow = [string, string];
+type Section = {title: string; rows: ShortcutRow[]};
+
+function toolLabel(s: string): string {
+ return s.split(' \u2014 ')[0].trim();
+}
+
+export function openShortcutsDialog(): void {
+ if (document.querySelector('.shortcuts-dialog')) return;
+
+ const backdrop = document.createElement('div');
+ backdrop.className = 'shortcuts-dialog-backdrop';
+
+ const dialog = document.createElement('div');
+ dialog.className = 'shortcuts-dialog';
+ dialog.setAttribute('role', 'dialog');
+ dialog.setAttribute('aria-modal', 'true');
+ dialog.setAttribute('aria-label', t.help.keyboardShortcuts);
+
+ const header = document.createElement('div');
+ header.className = 'shortcuts-dialog__header';
+
+ const title = document.createElement('h2');
+ title.className = 'shortcuts-dialog__title';
+ title.textContent = t.help.keyboardShortcuts;
+
+ const closeBtn = document.createElement('button');
+ closeBtn.type = 'button';
+ closeBtn.className = 'shortcuts-dialog__close';
+ closeBtn.textContent = '\u00d7';
+ closeBtn.title = t.help.close;
+ header.append(title, closeBtn);
+
+ const sections: Section[] = [
+ {
+ title: t.menu.file,
+ rows: [
+ [`${modKey}+N`, t.file.new],
+ [`${modKey}+O`, t.file.open],
+ [`${modKey}+S`, t.file.save],
+ ],
+ },
+ {
+ title: t.menu.edit,
+ rows: [
+ [`${modKey}+Z`, t.edit.undo],
+ [`${modKey}+Shift+Z`, t.edit.redo],
+ [`${modKey}+C`, t.edit.copy],
+ [`${modKey}+X`, t.edit.cut],
+ [`${modKey}+V`, t.edit.paste],
+ ['Delete', t.edit.clearLayer],
+ ],
+ },
+ {
+ title: t.help.sectionView,
+ rows: [
+ [`${modKey}+=`, t.help.zoomIn],
+ [`${modKey}+\u2212`, t.help.zoomOut],
+ [`${modKey}+0`, t.help.zoomReset],
+ ],
+ },
+ {
+ title: t.help.sectionTools,
+ rows: [
+ ['S', toolLabel(t.toolbar.toolSelect)],
+ ['P', toolLabel(t.toolbar.toolPencil)],
+ ['E', toolLabel(t.toolbar.toolEraser)],
+ ['C', toolLabel(t.toolbar.toolSampler)],
+ ['B', toolLabel(t.toolbar.toolFillBucket)],
+ ['W', toolLabel(t.toolbar.toolWeb)],
+ ['R', toolLabel(t.toolbar.toolRectangle)],
+ ['O', toolLabel(t.toolbar.toolEllipse)],
+ ['T', toolLabel(t.toolbar.toolText)],
+ ],
+ },
+ {
+ title: t.help.sectionCanvas,
+ rows: [
+ ['I', toolLabel(t.toolbar.invertColors)],
+ ['H', toolLabel(t.toolbar.mirrorH)],
+ ['V', toolLabel(t.toolbar.mirrorV)],
+ [',', toolLabel(t.toolbar.rotateLeft)],
+ ['.', toolLabel(t.toolbar.rotateRight)],
+ ['[', t.help.sizeDecrease],
+ [']', t.help.sizeIncrease],
+ ],
+ },
+ ];
+
+ const body = document.createElement('div');
+ body.className = 'shortcuts-dialog__body';
+
+ for (const section of sections) {
+ const col = document.createElement('div');
+ col.className = 'shortcuts-section';
+
+ const sectionTitle = document.createElement('h3');
+ sectionTitle.className = 'shortcuts-section__title';
+ sectionTitle.textContent = section.title;
+
+ const rows = document.createElement('div');
+ rows.className = 'shortcuts-section__rows';
+
+ for (const [key, desc] of section.rows) {
+ const row = document.createElement('div');
+ row.className = 'shortcuts-row';
+
+ const kbd = document.createElement('kbd');
+ kbd.className = 'shortcuts-row__key';
+ kbd.textContent = key;
+
+ const descEl = document.createElement('span');
+ descEl.className = 'shortcuts-row__desc';
+ descEl.textContent = desc;
+
+ row.append(kbd, descEl);
+ rows.append(row);
+ }
+
+ col.append(sectionTitle, rows);
+ body.append(col);
+ }
+
+ dialog.append(header, body);
+ document.body.append(backdrop, dialog);
+
+ function close(): void {
+ backdrop.remove();
+ dialog.remove();
+ document.removeEventListener('keydown', onKey);
+ }
+
+ function onKey(e: KeyboardEvent): void {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ close();
+ }
+ }
+
+ closeBtn.addEventListener('click', close);
+ backdrop.addEventListener('click', close);
+ document.addEventListener('keydown', onKey);
+ closeBtn.focus();
+}
diff --git a/src/ui/status-toasts.ts b/src/ui/status-toasts.ts
new file mode 100644
index 0000000..49e8312
--- /dev/null
+++ b/src/ui/status-toasts.ts
@@ -0,0 +1,29 @@
+import {t} from '../core/i18n';
+import type {StatusReporter, StatusTone} from '../types';
+
+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) return error.message;
+ return t.common.somethingWentWrong;
+}
diff --git a/src/ui/text-toolbar.ts b/src/ui/text-toolbar.ts
new file mode 100644
index 0000000..6e394ab
--- /dev/null
+++ b/src/ui/text-toolbar.ts
@@ -0,0 +1,201 @@
+import type {SizeWithPosition, TextToolbarDefaults} from '../types';
+
+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/src/ui/toolbar-controls.ts b/src/ui/toolbar-controls.ts
new file mode 100644
index 0000000..6bc1f29
--- /dev/null
+++ b/src/ui/toolbar-controls.ts
@@ -0,0 +1,123 @@
+import type {CheckboxControl, RangeControl} from '../types';
+
+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/src/ui/toolbar-sections.ts b/src/ui/toolbar-sections.ts
new file mode 100644
index 0000000..2cb2927
--- /dev/null
+++ b/src/ui/toolbar-sections.ts
@@ -0,0 +1,361 @@
+import {t} from '../core/i18n';
+import type {WebDraftEditor} from '../core/webdraft-editor';
+import {StatusReporter, Tool} from '../types';
+import {ToolbarSection, ToolConfig} from '../types/toolbar.ts';
+import {openCameraPanel} from './camera-panel';
+import {createColorPicker} from './color-picker';
+import {
+ createCheckboxControl,
+ createCommandButton,
+ createGrid,
+ createNumberInput,
+ createRangeControl,
+ createToolbarSection,
+} from './toolbar-controls';
+
+const tools: ToolConfig[] = [
+ {
+ id: Tool.Select,
+ label: t.toolbar.toolSelect,
+ icon: ' ',
+ },
+ {
+ id: Tool.Pencil,
+ label: t.toolbar.toolPencil,
+ icon: ' ',
+ },
+ {
+ id: Tool.Eraser,
+ label: t.toolbar.toolEraser,
+ icon: ' ',
+ },
+ {
+ id: Tool.Sampler,
+ label: t.toolbar.toolSampler,
+ icon: ' ',
+ },
+ {
+ id: Tool.FillBucket,
+ label: t.toolbar.toolFillBucket,
+ icon: ' ',
+ },
+ {
+ id: Tool.Web,
+ label: t.toolbar.toolWeb,
+ icon: ' ',
+ },
+ {
+ id: Tool.Rectangle,
+ label: t.toolbar.toolRectangle,
+ icon: ' ',
+ },
+ {
+ id: Tool.Ellipse,
+ label: t.toolbar.toolEllipse,
+ icon: ' ',
+ },
+ {
+ id: Tool.Text,
+ label: t.toolbar.toolText,
+ 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;
+}
+
+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';
+
+ 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.append(createSvgIcon(tool.icon));
+ button.addEventListener('click', () => {
+ editor.setTool(tool.id);
+ });
+
+ toolButtons.set(tool.id, button);
+ toolGroup.append(button);
+ }
+
+ const transformGroup = createGrid(
+ 'toolbar__group',
+ createEditButton(
+ t.toolbar.invertColors,
+ ' ',
+ () => editor.invertActiveLayer(),
+ ),
+ createEditButton(
+ t.toolbar.rotateLeft,
+ ' ',
+ () => editor.rotateActiveLayer('left'),
+ ),
+ createEditButton(
+ t.toolbar.rotateRight,
+ ' ',
+ () => editor.rotateActiveLayer('right'),
+ ),
+ createEditButton(
+ t.toolbar.mirrorH,
+ ' ',
+ () => editor.mirrorActiveLayer('horizontal'),
+ ),
+ createEditButton(
+ t.toolbar.mirrorV,
+ ' ',
+ () => editor.mirrorActiveLayer('vertical'),
+ ),
+ );
+
+ return {
+ element: createToolbarSection(toolGroup, transformGroup),
+ 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: t.toolbar.size,
+ min: 1,
+ max: 120,
+ value: editor.state.size,
+ onChange: (value) => editor.setSize(value),
+ });
+
+ const webSensitivityControl = createRangeControl({
+ label: t.toolbar.webSensitivity,
+ min: 20,
+ max: 260,
+ value: editor.state.webSensitivity,
+ onChange: (value) => editor.setWebSensitivity(value),
+ });
+
+ const colorPicker = createColorPicker({
+ label: t.toolbar.color,
+ value: editor.state.color,
+ onChange: (color) => editor.setColor(color),
+ });
+
+ const fillToggle = createCheckboxControl(t.toolbar.fillShapes, editor.state.fillEnabled, (enabled) => {
+ editor.setFillEnabled(enabled);
+ });
+
+ const fillColorPicker = createColorPicker({
+ label: t.toolbar.fillColor,
+ value: editor.state.fillColor,
+ onChange: (color) => editor.setFillColor(color),
+ });
+
+ const fillOpacityControl = createRangeControl({
+ label: t.toolbar.fillOpacity,
+ min: 0,
+ max: 100,
+ value: editor.state.fillOpacity,
+ onChange: (value) => editor.setFillOpacity(value),
+ });
+
+ const fillToleranceControl = createRangeControl({
+ label: t.toolbar.bucketTolerance,
+ min: 0,
+ max: 255,
+ value: editor.state.fillTolerance,
+ 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,
+ fillToggle.element,
+ fillColorPicker,
+ fillOpacityControl.element,
+ fillToleranceControl.element,
+ sizeControl.element,
+ webSensitivityControl.element,
+ ),
+ sync: () => {
+ syncVisibility();
+ colorPicker.setValue(editor.state.color);
+ 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);
+ },
+ };
+}
+
+export function createShadowSection(editor: WebDraftEditor): ToolbarSection {
+ const shadowToggle = createCheckboxControl(t.toolbar.shadow, editor.state.shadowEnabled, (enabled) => {
+ editor.setShadowEnabled(enabled);
+ });
+
+ const shadowColorPicker = createColorPicker({
+ label: t.toolbar.shadowColor,
+ value: editor.state.shadowColor,
+ onChange: (color) => editor.setShadowColor(color),
+ });
+
+ const shadowBlurControl = createRangeControl({
+ label: t.toolbar.shadowBlur,
+ min: 0,
+ max: 80,
+ value: editor.state.shadowBlur,
+ onChange: (value) => editor.setShadowBlur(value),
+ });
+
+ const shadowXControl = createRangeControl({
+ label: t.toolbar.shadowX,
+ min: -120,
+ max: 120,
+ value: editor.state.shadowOffsetX,
+ onChange: (value) => editor.setShadowOffsetX(value),
+ });
+
+ const shadowYControl = createRangeControl({
+ label: t.toolbar.shadowY,
+ min: -120,
+ max: 120,
+ value: editor.state.shadowOffsetY,
+ 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,
+ shadowColorPicker,
+ shadowBlurControl.element,
+ shadowXControl.element,
+ shadowYControl.element,
+ ),
+ sync: () => {
+ syncShadowDetails();
+ 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 createEditSection(editor: WebDraftEditor): ToolbarSection {
+ const clearButton = createEditButton(
+ t.toolbar.clear,
+ ' ',
+ () => editor.clear(),
+ );
+ const undoButton = createEditButton(
+ t.toolbar.undo,
+ ' ',
+ () => editor.undo(),
+ );
+ const redoButton = createEditButton(
+ t.toolbar.redo,
+ ' ',
+ () => editor.redo(),
+ );
+ const copyButton = createEditButton(
+ t.toolbar.copy,
+ ' ',
+ () => editor.copySelection(),
+ );
+ const cutButton = createEditButton(
+ t.toolbar.cut,
+ ' ',
+ () => editor.cutSelection(),
+ );
+ const pasteButton = createEditButton(
+ t.toolbar.paste,
+ ' ',
+ () => editor.pasteSelection(),
+ );
+ return {
+ element: createToolbarSection(
+ createGrid('command-grid', clearButton, undoButton, redoButton, copyButton, cutButton, pasteButton),
+ ),
+ sync: () => {
+ undoButton.disabled = !editor.canUndo;
+ redoButton.disabled = !editor.canRedo;
+ copyButton.disabled = !editor.hasSelection;
+ cutButton.disabled = !editor.hasSelection;
+ pasteButton.disabled = !editor.canPaste;
+ },
+ };
+}
+
+export function createFileSection(editor: WebDraftEditor, status: StatusReporter): ToolbarSection {
+ const cameraButton = createCommandButton(t.toolbar.camera, () => {
+ openCameraPanel(editor, status);
+ });
+
+ return {
+ element: createToolbarSection(cameraButton),
+ sync: () => {},
+ };
+}
+
+export function syncToolbarSections(sections: ToolbarSection[]): void {
+ for (const section of sections) {
+ section.sync();
+ }
+}
diff --git a/src/ui/toolbar.ts b/src/ui/toolbar.ts
new file mode 100644
index 0000000..9553f7f
--- /dev/null
+++ b/src/ui/toolbar.ts
@@ -0,0 +1,109 @@
+import {parseWdraftBinary} from '../core/project-file';
+import {t} from '../core/i18n';
+import type {WebDraftEditor} from '../core/webdraft-editor';
+import {StatusReporter} from '../types';
+import {bindKeyboardShortcuts} from './keyboard-shortcuts';
+import {openSaveDialog} from './save-dialog';
+import {openShortcutsDialog} from './shortcuts-dialog';
+import {getErrorMessage} from './status-toasts';
+import {
+ createFileSection,
+ createShadowSection,
+ createStyleSection,
+ createToolSection,
+ syncToolbarSections,
+} from './toolbar-sections';
+
+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, status);
+ const exportImage = async () => {
+ try {
+ const blob = await editor.exportPng();
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ link.href = url;
+ link.download = t.file.imageFilename;
+ link.click();
+ URL.revokeObjectURL(url);
+ status.show(t.toolbar.pngExportedOk, 'success');
+ } catch (error) {
+ status.show(getErrorMessage(error), 'error');
+ }
+ };
+
+ const sections = [
+ createToolSection(editor),
+ createStyleSection(editor),
+ createShadowSection(editor),
+ createFileSection(editor, status),
+ ];
+
+ editor.addEventListener('change', () => syncToolbarSections(sections));
+ syncToolbarSections(sections);
+
+ toolbar.append(title, fileInput, ...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();
+ }),
+ openShortcutsDialog,
+ });
+
+ return toolbar;
+}
+
+function createFileInput(editor: WebDraftEditor, status: StatusReporter): HTMLInputElement {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = 'image/*,.wdraft';
+ fileInput.className = 'visually-hidden';
+ fileInput.addEventListener('change', () => {
+ const file = fileInput.files?.[0];
+ if (!file) return;
+ void (async () => {
+ try {
+ if (file.name.endsWith('.wdraft')) {
+ const buffer = await file.arrayBuffer();
+ const parsed = parseWdraftBinary(buffer);
+ await editor.importProject(parsed);
+ status.show(t.file.openedOk, 'success');
+ } else {
+ await editor.importImage(file);
+ status.show(t.toolbar.imageImportedOk, 'success');
+ }
+ } catch (error) {
+ status.show(getErrorMessage(error), 'error');
+ } finally {
+ fileInput.value = '';
+ }
+ })();
+ });
+
+ return fileInput;
+}
diff --git a/tests/editor-geometry.test.mjs b/tests/editor-geometry.test.mjs
new file mode 100644
index 0000000..eda0354
--- /dev/null
+++ b/tests/editor-geometry.test.mjs
@@ -0,0 +1,68 @@
+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, getClippedPasteBounds, 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,
+ });
+});
+
+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.');
diff --git a/tests/flood-fill.test.mjs b/tests/flood-fill.test.mjs
new file mode 100644
index 0000000..57c8ba8
--- /dev/null
+++ b/tests/flood-fill.test.mjs
@@ -0,0 +1,128 @@
+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('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('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});
+});
+
+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/tests/history-manager.test.mjs b/tests/history-manager.test.mjs
new file mode 100644
index 0000000..89e3f3c
--- /dev/null
+++ b/tests/history-manager.test.mjs
@@ -0,0 +1,55 @@
+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');
+});
+
+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.');
diff --git a/tests/layer-transforms.test.mjs b/tests/layer-transforms.test.mjs
new file mode 100644
index 0000000..2671e8c
--- /dev/null
+++ b/tests/layer-transforms.test.mjs
@@ -0,0 +1,107 @@
+import assert from 'node:assert/strict';
+import {importTypescriptModule, test} from './test-support.mjs';
+
+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.');
+
+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/tests/test-support.mjs b/tests/test-support.mjs
new file mode 100644
index 0000000..9f8eaca
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..48d633f
--- /dev/null
+++ b/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/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 fa56790..0000000
Binary files a/vendor/fontawesome/webfonts/fa-brands-400.eot and /dev/null differ
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 6bef897..0000000
Binary files a/vendor/fontawesome/webfonts/fa-brands-400.ttf and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-brands-400.woff b/vendor/fontawesome/webfonts/fa-brands-400.woff
deleted file mode 100644
index 2b5d946..0000000
Binary files a/vendor/fontawesome/webfonts/fa-brands-400.woff and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-brands-400.woff2 b/vendor/fontawesome/webfonts/fa-brands-400.woff2
deleted file mode 100644
index 7008b03..0000000
Binary files a/vendor/fontawesome/webfonts/fa-brands-400.woff2 and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-regular-400.eot b/vendor/fontawesome/webfonts/fa-regular-400.eot
deleted file mode 100644
index e7e4726..0000000
Binary files a/vendor/fontawesome/webfonts/fa-regular-400.eot and /dev/null differ
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 d61aedd..0000000
Binary files a/vendor/fontawesome/webfonts/fa-regular-400.ttf and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-regular-400.woff b/vendor/fontawesome/webfonts/fa-regular-400.woff
deleted file mode 100644
index 8153f4f..0000000
Binary files a/vendor/fontawesome/webfonts/fa-regular-400.woff and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-regular-400.woff2 b/vendor/fontawesome/webfonts/fa-regular-400.woff2
deleted file mode 100644
index 44b010a..0000000
Binary files a/vendor/fontawesome/webfonts/fa-regular-400.woff2 and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-solid-900.eot b/vendor/fontawesome/webfonts/fa-solid-900.eot
deleted file mode 100644
index af7eea4..0000000
Binary files a/vendor/fontawesome/webfonts/fa-solid-900.eot and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-solid-900.svg b/vendor/fontawesome/webfonts/fa-solid-900.svg
deleted file mode 100644
index 41d8bee..0000000
--- a/vendor/fontawesome/webfonts/fa-solid-900.svg
+++ /dev/null
@@ -1,1518 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vendor/fontawesome/webfonts/fa-solid-900.ttf b/vendor/fontawesome/webfonts/fa-solid-900.ttf
deleted file mode 100644
index 5f00b17..0000000
Binary files a/vendor/fontawesome/webfonts/fa-solid-900.ttf and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-solid-900.woff b/vendor/fontawesome/webfonts/fa-solid-900.woff
deleted file mode 100644
index 0f0fa09..0000000
Binary files a/vendor/fontawesome/webfonts/fa-solid-900.woff and /dev/null differ
diff --git a/vendor/fontawesome/webfonts/fa-solid-900.woff2 b/vendor/fontawesome/webfonts/fa-solid-900.woff2
deleted file mode 100644
index 64840a6..0000000
Binary files a/vendor/fontawesome/webfonts/fa-solid-900.woff2 and /dev/null differ
diff --git a/vendor/html2canvas/html2canvas.js b/vendor/html2canvas/html2canvas.js
deleted file mode 100644
index 3a44bc3..0000000
--- a/vendor/html2canvas/html2canvas.js
+++ /dev/null
@@ -1,2868 +0,0 @@
-/*
- html2canvas 0.4.1
- Copyright (c) 2013 Niklas von Hertzen
-
- Released under MIT License
-*/
-
-(function(window, document, undefined){
-
-"use strict";
-
-var _html2canvas = {},
-previousElement,
-computedCSS,
-html2canvas;
-
-_html2canvas.Util = {};
-
-_html2canvas.Util.log = function(a) {
- if (_html2canvas.logging && window.console && window.console.log) {
- window.console.log(a);
- }
-};
-
-_html2canvas.Util.trimText = (function(isNative){
- return function(input) {
- return isNative ? isNative.apply(input) : ((input || '') + '').replace( /^\s+|\s+$/g , '' );
- };
-})(String.prototype.trim);
-
-_html2canvas.Util.asFloat = function(v) {
- return parseFloat(v);
-};
-
-(function() {
- // TODO: support all possible length values
- var TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
- var TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
- _html2canvas.Util.parseTextShadows = function (value) {
- if (!value || value === 'none') {
- return [];
- }
-
- // find multiple shadow declarations
- var shadows = value.match(TEXT_SHADOW_PROPERTY),
- results = [];
- for (var i = 0; shadows && (i < shadows.length); i++) {
- var s = shadows[i].match(TEXT_SHADOW_VALUES);
- results.push({
- color: s[0],
- offsetX: s[1] ? s[1].replace('px', '') : 0,
- offsetY: s[2] ? s[2].replace('px', '') : 0,
- blur: s[3] ? s[3].replace('px', '') : 0
- });
- }
- return results;
- };
-})();
-
-
-_html2canvas.Util.parseBackgroundImage = function (value) {
- var whitespace = ' \r\n\t',
- method, definition, prefix, prefix_i, block, results = [],
- c, mode = 0, numParen = 0, quote, args;
-
- var appendResult = function(){
- if(method) {
- if(definition.substr( 0, 1 ) === '"') {
- definition = definition.substr( 1, definition.length - 2 );
- }
- if(definition) {
- args.push(definition);
- }
- if(method.substr( 0, 1 ) === '-' &&
- (prefix_i = method.indexOf( '-', 1 ) + 1) > 0) {
- prefix = method.substr( 0, prefix_i);
- method = method.substr( prefix_i );
- }
- results.push({
- prefix: prefix,
- method: method.toLowerCase(),
- value: block,
- args: args
- });
- }
- args = []; //for some odd reason, setting .length = 0 didn't work in safari
- method =
- prefix =
- definition =
- block = '';
- };
-
- appendResult();
- for(var i = 0, ii = value.length; i -1){
- continue;
- }
- switch(c) {
- case '"':
- if(!quote) {
- quote = c;
- }
- else if(quote === c) {
- quote = null;
- }
- break;
-
- case '(':
- if(quote) { break; }
- else if(mode === 0) {
- mode = 1;
- block += c;
- continue;
- } else {
- numParen++;
- }
- break;
-
- case ')':
- if(quote) { break; }
- else if(mode === 1) {
- if(numParen === 0) {
- mode = 0;
- block += c;
- appendResult();
- continue;
- } else {
- numParen--;
- }
- }
- break;
-
- case ',':
- if(quote) { break; }
- else if(mode === 0) {
- appendResult();
- continue;
- }
- else if (mode === 1) {
- if(numParen === 0 && !method.match(/^url$/i)) {
- args.push(definition);
- definition = '';
- block += c;
- continue;
- }
- }
- break;
- }
-
- block += c;
- if(mode === 0) { method += c; }
- else { definition += c; }
- }
- appendResult();
-
- return results;
-};
-
-_html2canvas.Util.Bounds = function (element) {
- var clientRect, bounds = {};
-
- if (element.getBoundingClientRect){
- clientRect = element.getBoundingClientRect();
-
- // TODO add scroll position to bounds, so no scrolling of window necessary
- bounds.top = clientRect.top;
- bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
- bounds.left = clientRect.left;
-
- bounds.width = element.offsetWidth;
- bounds.height = element.offsetHeight;
- }
-
- return bounds;
-};
-
-// TODO ideally, we'd want everything to go through this function instead of Util.Bounds,
-// but would require further work to calculate the correct positions for elements with offsetParents
-_html2canvas.Util.OffsetBounds = function (element) {
- var parent = element.offsetParent ? _html2canvas.Util.OffsetBounds(element.offsetParent) : {top: 0, left: 0};
-
- return {
- top: element.offsetTop + parent.top,
- bottom: element.offsetTop + element.offsetHeight + parent.top,
- left: element.offsetLeft + parent.left,
- width: element.offsetWidth,
- height: element.offsetHeight
- };
-};
-
-function toPX(element, attribute, value ) {
- var rsLeft = element.runtimeStyle && element.runtimeStyle[attribute],
- left,
- style = element.style;
-
- // Check if we are not dealing with pixels, (Opera has issues with this)
- // Ported from jQuery css.js
- // From the awesome hack by Dean Edwards
- // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
-
- // If we're not dealing with a regular pixel number
- // but a number that has a weird ending, we need to convert it to pixels
-
- if ( !/^-?[0-9]+\.?[0-9]*(?:px)?$/i.test( value ) && /^-?\d/.test(value) ) {
- // Remember the original values
- left = style.left;
-
- // Put in the new values to get a computed value out
- if (rsLeft) {
- element.runtimeStyle.left = element.currentStyle.left;
- }
- style.left = attribute === "fontSize" ? "1em" : (value || 0);
- value = style.pixelLeft + "px";
-
- // Revert the changed values
- style.left = left;
- if (rsLeft) {
- element.runtimeStyle.left = rsLeft;
- }
- }
-
- if (!/^(thin|medium|thick)$/i.test(value)) {
- return Math.round(parseFloat(value)) + "px";
- }
-
- return value;
-}
-
-function asInt(val) {
- return parseInt(val, 10);
-}
-
-function parseBackgroundSizePosition(value, element, attribute, index) {
- value = (value || '').split(',');
- value = value[index || 0] || value[0] || 'auto';
- value = _html2canvas.Util.trimText(value).split(' ');
-
- if(attribute === 'backgroundSize' && (!value[0] || value[0].match(/cover|contain|auto/))) {
- //these values will be handled in the parent function
- } else {
- value[0] = (value[0].indexOf( "%" ) === -1) ? toPX(element, attribute + "X", value[0]) : value[0];
- if(value[1] === undefined) {
- if(attribute === 'backgroundSize') {
- value[1] = 'auto';
- return value;
- } else {
- // IE 9 doesn't return double digit always
- value[1] = value[0];
- }
- }
- value[1] = (value[1].indexOf("%") === -1) ? toPX(element, attribute + "Y", value[1]) : value[1];
- }
- return value;
-}
-
-_html2canvas.Util.getCSS = function (element, attribute, index) {
- if (previousElement !== element) {
- computedCSS = document.defaultView.getComputedStyle(element, null);
- }
-
- var value = computedCSS[attribute];
-
- if (/^background(Size|Position)$/.test(attribute)) {
- return parseBackgroundSizePosition(value, element, attribute, index);
- } else if (/border(Top|Bottom)(Left|Right)Radius/.test(attribute)) {
- var arr = value.split(" ");
- if (arr.length <= 1) {
- arr[1] = arr[0];
- }
- return arr.map(asInt);
- }
-
- return value;
-};
-
-_html2canvas.Util.resizeBounds = function( current_width, current_height, target_width, target_height, stretch_mode ){
- var target_ratio = target_width / target_height,
- current_ratio = current_width / current_height,
- output_width, output_height;
-
- if(!stretch_mode || stretch_mode === 'auto') {
- output_width = target_width;
- output_height = target_height;
- } else if(target_ratio < current_ratio ^ stretch_mode === 'contain') {
- output_height = target_height;
- output_width = target_height * current_ratio;
- } else {
- output_width = target_width;
- output_height = target_width / current_ratio;
- }
-
- return {
- width: output_width,
- height: output_height
- };
-};
-
-function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroundSize ) {
- var bgposition = _html2canvas.Util.getCSS( el, prop, imageIndex ) ,
- topPos,
- left,
- percentage,
- val;
-
- if (bgposition.length === 1){
- val = bgposition[0];
-
- bgposition = [];
-
- bgposition[0] = val;
- bgposition[1] = val;
- }
-
- if (bgposition[0].toString().indexOf("%") !== -1){
- percentage = (parseFloat(bgposition[0])/100);
- left = bounds.width * percentage;
- if(prop !== 'backgroundSize') {
- left -= (backgroundSize || image).width*percentage;
- }
- } else {
- if(prop === 'backgroundSize') {
- if(bgposition[0] === 'auto') {
- left = image.width;
- } else {
- if (/contain|cover/.test(bgposition[0])) {
- var resized = _html2canvas.Util.resizeBounds(image.width, image.height, bounds.width, bounds.height, bgposition[0]);
- left = resized.width;
- topPos = resized.height;
- } else {
- left = parseInt(bgposition[0], 10);
- }
- }
- } else {
- left = parseInt( bgposition[0], 10);
- }
- }
-
-
- if(bgposition[1] === 'auto') {
- topPos = left / image.width * image.height;
- } else if (bgposition[1].toString().indexOf("%") !== -1){
- percentage = (parseFloat(bgposition[1])/100);
- topPos = bounds.height * percentage;
- if(prop !== 'backgroundSize') {
- topPos -= (backgroundSize || image).height * percentage;
- }
-
- } else {
- topPos = parseInt(bgposition[1],10);
- }
-
- return [left, topPos];
-}
-
-_html2canvas.Util.BackgroundPosition = function( el, bounds, image, imageIndex, backgroundSize ) {
- var result = backgroundBoundsFactory( 'backgroundPosition', el, bounds, image, imageIndex, backgroundSize );
- return { left: result[0], top: result[1] };
-};
-
-_html2canvas.Util.BackgroundSize = function( el, bounds, image, imageIndex ) {
- var result = backgroundBoundsFactory( 'backgroundSize', el, bounds, image, imageIndex );
- return { width: result[0], height: result[1] };
-};
-
-_html2canvas.Util.Extend = function (options, defaults) {
- for (var key in options) {
- if (options.hasOwnProperty(key)) {
- defaults[key] = options[key];
- }
- }
- return defaults;
-};
-
-
-/*
- * Derived from jQuery.contents()
- * Copyright 2010, John Resig
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://jquery.org/license
- */
-_html2canvas.Util.Children = function( elem ) {
- var children;
- try {
- children = (elem.nodeName && elem.nodeName.toUpperCase() === "IFRAME") ? elem.contentDocument || elem.contentWindow.document : (function(array) {
- var ret = [];
- if (array !== null) {
- (function(first, second ) {
- var i = first.length,
- j = 0;
-
- if (typeof second.length === "number") {
- for (var l = second.length; j < l; j++) {
- first[i++] = second[j];
- }
- } else {
- while (second[j] !== undefined) {
- first[i++] = second[j++];
- }
- }
-
- first.length = i;
-
- return first;
- })(ret, array);
- }
- return ret;
- })(elem.childNodes);
-
- } catch (ex) {
- _html2canvas.Util.log("html2canvas.Util.Children failed with exception: " + ex.message);
- children = [];
- }
- return children;
-};
-
-_html2canvas.Util.isTransparent = function(backgroundColor) {
- return (backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)");
-};
-_html2canvas.Util.Font = (function () {
-
- var fontData = {};
-
- return function(font, fontSize, doc) {
- if (fontData[font + "-" + fontSize] !== undefined) {
- return fontData[font + "-" + fontSize];
- }
-
- var container = doc.createElement('div'),
- img = doc.createElement('img'),
- span = doc.createElement('span'),
- sampleText = 'Hidden Text',
- baseline,
- middle,
- metricsObj;
-
- container.style.visibility = "hidden";
- container.style.fontFamily = font;
- container.style.fontSize = fontSize;
- container.style.margin = 0;
- container.style.padding = 0;
-
- doc.body.appendChild(container);
-
- // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever (handtinywhite.gif)
- img.src = "data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
- img.width = 1;
- img.height = 1;
-
- img.style.margin = 0;
- img.style.padding = 0;
- img.style.verticalAlign = "baseline";
-
- span.style.fontFamily = font;
- span.style.fontSize = fontSize;
- span.style.margin = 0;
- span.style.padding = 0;
-
- span.appendChild(doc.createTextNode(sampleText));
- container.appendChild(span);
- container.appendChild(img);
- baseline = (img.offsetTop - span.offsetTop) + 1;
-
- container.removeChild(span);
- container.appendChild(doc.createTextNode(sampleText));
-
- container.style.lineHeight = "normal";
- img.style.verticalAlign = "super";
-
- middle = (img.offsetTop-container.offsetTop) + 1;
- metricsObj = {
- baseline: baseline,
- lineWidth: 1,
- middle: middle
- };
-
- fontData[font + "-" + fontSize] = metricsObj;
-
- doc.body.removeChild(container);
-
- return metricsObj;
- };
-})();
-
-(function(){
- var Util = _html2canvas.Util,
- Generate = {};
-
- _html2canvas.Generate = Generate;
-
- var reGradients = [
- /^(-webkit-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/,
- /^(-o-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/,
- /^(-webkit-gradient)\((linear|radial),\s((?:\d{1,3}%?)\s(?:\d{1,3}%?),\s(?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)\-]+)\)$/,
- /^(-moz-linear-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)]+)\)$/,
- /^(-webkit-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/,
- /^(-moz-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s?([a-z\-]*)([\w\d\.\s,%\(\)]+)\)$/,
- /^(-o-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/
- ];
-
- /*
- * TODO: Add IE10 vendor prefix (-ms) support
- * TODO: Add W3C gradient (linear-gradient) support
- * TODO: Add old Webkit -webkit-gradient(radial, ...) support
- * TODO: Maybe some RegExp optimizations are possible ;o)
- */
- Generate.parseGradient = function(css, bounds) {
- var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3, tl,tr,br,bl;
-
- for(i = 0; i < len; i+=1){
- m1 = css.match(reGradients[i]);
- if(m1) {
- break;
- }
- }
-
- if(m1) {
- switch(m1[1]) {
- case '-webkit-linear-gradient':
- case '-o-linear-gradient':
-
- gradient = {
- type: 'linear',
- x0: null,
- y0: null,
- x1: null,
- y1: null,
- colorStops: []
- };
-
- // get coordinates
- m2 = m1[2].match(/\w+/g);
- if(m2){
- m2Len = m2.length;
- for(i = 0; i < m2Len; i+=1){
- switch(m2[i]) {
- case 'top':
- gradient.y0 = 0;
- gradient.y1 = bounds.height;
- break;
-
- case 'right':
- gradient.x0 = bounds.width;
- gradient.x1 = 0;
- break;
-
- case 'bottom':
- gradient.y0 = bounds.height;
- gradient.y1 = 0;
- break;
-
- case 'left':
- gradient.x0 = 0;
- gradient.x1 = bounds.width;
- break;
- }
- }
- }
- if(gradient.x0 === null && gradient.x1 === null){ // center
- gradient.x0 = gradient.x1 = bounds.width / 2;
- }
- if(gradient.y0 === null && gradient.y1 === null){ // center
- gradient.y0 = gradient.y1 = bounds.height / 2;
- }
-
- // get colors and stops
- m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g);
- if(m2){
- m2Len = m2.length;
- step = 1 / Math.max(m2Len - 1, 1);
- for(i = 0; i < m2Len; i+=1){
- m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/);
- if(m3[2]){
- stop = parseFloat(m3[2]);
- if(m3[3] === '%'){
- stop /= 100;
- } else { // px - stupid opera
- stop /= bounds.width;
- }
- } else {
- stop = i * step;
- }
- gradient.colorStops.push({
- color: m3[1],
- stop: stop
- });
- }
- }
- break;
-
- case '-webkit-gradient':
-
- gradient = {
- type: m1[2] === 'radial' ? 'circle' : m1[2], // TODO: Add radial gradient support for older mozilla definitions
- x0: 0,
- y0: 0,
- x1: 0,
- y1: 0,
- colorStops: []
- };
-
- // get coordinates
- m2 = m1[3].match(/(\d{1,3})%?\s(\d{1,3})%?,\s(\d{1,3})%?\s(\d{1,3})%?/);
- if(m2){
- gradient.x0 = (m2[1] * bounds.width) / 100;
- gradient.y0 = (m2[2] * bounds.height) / 100;
- gradient.x1 = (m2[3] * bounds.width) / 100;
- gradient.y1 = (m2[4] * bounds.height) / 100;
- }
-
- // get colors and stops
- m2 = m1[4].match(/((?:from|to|color-stop)\((?:[0-9\.]+,\s)?(?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)\))+/g);
- if(m2){
- m2Len = m2.length;
- for(i = 0; i < m2Len; i+=1){
- m3 = m2[i].match(/(from|to|color-stop)\(([0-9\.]+)?(?:,\s)?((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\)/);
- stop = parseFloat(m3[2]);
- if(m3[1] === 'from') {
- stop = 0.0;
- }
- if(m3[1] === 'to') {
- stop = 1.0;
- }
- gradient.colorStops.push({
- color: m3[3],
- stop: stop
- });
- }
- }
- break;
-
- case '-moz-linear-gradient':
-
- gradient = {
- type: 'linear',
- x0: 0,
- y0: 0,
- x1: 0,
- y1: 0,
- colorStops: []
- };
-
- // get coordinates
- m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/);
-
- // m2[1] == 0% -> left
- // m2[1] == 50% -> center
- // m2[1] == 100% -> right
-
- // m2[2] == 0% -> top
- // m2[2] == 50% -> center
- // m2[2] == 100% -> bottom
-
- if(m2){
- gradient.x0 = (m2[1] * bounds.width) / 100;
- gradient.y0 = (m2[2] * bounds.height) / 100;
- gradient.x1 = bounds.width - gradient.x0;
- gradient.y1 = bounds.height - gradient.y0;
- }
-
- // get colors and stops
- m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}%)?)+/g);
- if(m2){
- m2Len = m2.length;
- step = 1 / Math.max(m2Len - 1, 1);
- for(i = 0; i < m2Len; i+=1){
- m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%)?/);
- if(m3[2]){
- stop = parseFloat(m3[2]);
- if(m3[3]){ // percentage
- stop /= 100;
- }
- } else {
- stop = i * step;
- }
- gradient.colorStops.push({
- color: m3[1],
- stop: stop
- });
- }
- }
- break;
-
- case '-webkit-radial-gradient':
- case '-moz-radial-gradient':
- case '-o-radial-gradient':
-
- gradient = {
- type: 'circle',
- x0: 0,
- y0: 0,
- x1: bounds.width,
- y1: bounds.height,
- cx: 0,
- cy: 0,
- rx: 0,
- ry: 0,
- colorStops: []
- };
-
- // center
- m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/);
- if(m2){
- gradient.cx = (m2[1] * bounds.width) / 100;
- gradient.cy = (m2[2] * bounds.height) / 100;
- }
-
- // size
- m2 = m1[3].match(/\w+/);
- m3 = m1[4].match(/[a-z\-]*/);
- if(m2 && m3){
- switch(m3[0]){
- case 'farthest-corner':
- case 'cover': // is equivalent to farthest-corner
- case '': // mozilla removes "cover" from definition :(
- tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
- tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
- br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
- bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2));
- gradient.rx = gradient.ry = Math.max(tl, tr, br, bl);
- break;
- case 'closest-corner':
- tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
- tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
- br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
- bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2));
- gradient.rx = gradient.ry = Math.min(tl, tr, br, bl);
- break;
- case 'farthest-side':
- if(m2[0] === 'circle'){
- gradient.rx = gradient.ry = Math.max(
- gradient.cx,
- gradient.cy,
- gradient.x1 - gradient.cx,
- gradient.y1 - gradient.cy
- );
- } else { // ellipse
-
- gradient.type = m2[0];
-
- gradient.rx = Math.max(
- gradient.cx,
- gradient.x1 - gradient.cx
- );
- gradient.ry = Math.max(
- gradient.cy,
- gradient.y1 - gradient.cy
- );
- }
- break;
- case 'closest-side':
- case 'contain': // is equivalent to closest-side
- if(m2[0] === 'circle'){
- gradient.rx = gradient.ry = Math.min(
- gradient.cx,
- gradient.cy,
- gradient.x1 - gradient.cx,
- gradient.y1 - gradient.cy
- );
- } else { // ellipse
-
- gradient.type = m2[0];
-
- gradient.rx = Math.min(
- gradient.cx,
- gradient.x1 - gradient.cx
- );
- gradient.ry = Math.min(
- gradient.cy,
- gradient.y1 - gradient.cy
- );
- }
- break;
-
- // TODO: add support for "30px 40px" sizes (webkit only)
- }
- }
-
- // color stops
- m2 = m1[5].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g);
- if(m2){
- m2Len = m2.length;
- step = 1 / Math.max(m2Len - 1, 1);
- for(i = 0; i < m2Len; i+=1){
- m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/);
- if(m3[2]){
- stop = parseFloat(m3[2]);
- if(m3[3] === '%'){
- stop /= 100;
- } else { // px - stupid opera
- stop /= bounds.width;
- }
- } else {
- stop = i * step;
- }
- gradient.colorStops.push({
- color: m3[1],
- stop: stop
- });
- }
- }
- break;
- }
- }
-
- return gradient;
- };
-
- function addScrollStops(grad) {
- return function(colorStop) {
- try {
- grad.addColorStop(colorStop.stop, colorStop.color);
- }
- catch(e) {
- Util.log(['failed to add color stop: ', e, '; tried to add: ', colorStop]);
- }
- };
- }
-
- Generate.Gradient = function(src, bounds) {
- if(bounds.width === 0 || bounds.height === 0) {
- return;
- }
-
- var canvas = document.createElement('canvas'),
- ctx = canvas.getContext('2d'),
- gradient, grad;
-
- canvas.width = bounds.width;
- canvas.height = bounds.height;
-
- // TODO: add support for multi defined background gradients
- gradient = _html2canvas.Generate.parseGradient(src, bounds);
-
- if(gradient) {
- switch(gradient.type) {
- case 'linear':
- grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
- gradient.colorStops.forEach(addScrollStops(grad));
- ctx.fillStyle = grad;
- ctx.fillRect(0, 0, bounds.width, bounds.height);
- break;
-
- case 'circle':
- grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx);
- gradient.colorStops.forEach(addScrollStops(grad));
- ctx.fillStyle = grad;
- ctx.fillRect(0, 0, bounds.width, bounds.height);
- break;
-
- case 'ellipse':
- var canvasRadial = document.createElement('canvas'),
- ctxRadial = canvasRadial.getContext('2d'),
- ri = Math.max(gradient.rx, gradient.ry),
- di = ri * 2;
-
- canvasRadial.width = canvasRadial.height = di;
-
- grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri);
- gradient.colorStops.forEach(addScrollStops(grad));
-
- ctxRadial.fillStyle = grad;
- ctxRadial.fillRect(0, 0, di, di);
-
- ctx.fillStyle = gradient.colorStops[gradient.colorStops.length - 1].color;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry);
- break;
- }
- }
-
- return canvas;
- };
-
- Generate.ListAlpha = function(number) {
- var tmp = "",
- modulus;
-
- do {
- modulus = number % 26;
- tmp = String.fromCharCode((modulus) + 64) + tmp;
- number = number / 26;
- }while((number*26) > 26);
-
- return tmp;
- };
-
- Generate.ListRoman = function(number) {
- var romanArray = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"],
- decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
- roman = "",
- v,
- len = romanArray.length;
-
- if (number <= 0 || number >= 4000) {
- return number;
- }
-
- for (v=0; v < len; v+=1) {
- while (number >= decimal[v]) {
- number -= decimal[v];
- roman += romanArray[v];
- }
- }
-
- return roman;
- };
-})();
-function h2cRenderContext(width, height) {
- var storage = [];
- return {
- storage: storage,
- width: width,
- height: height,
- clip: function() {
- storage.push({
- type: "function",
- name: "clip",
- 'arguments': arguments
- });
- },
- translate: function() {
- storage.push({
- type: "function",
- name: "translate",
- 'arguments': arguments
- });
- },
- fill: function() {
- storage.push({
- type: "function",
- name: "fill",
- 'arguments': arguments
- });
- },
- save: function() {
- storage.push({
- type: "function",
- name: "save",
- 'arguments': arguments
- });
- },
- restore: function() {
- storage.push({
- type: "function",
- name: "restore",
- 'arguments': arguments
- });
- },
- fillRect: function () {
- storage.push({
- type: "function",
- name: "fillRect",
- 'arguments': arguments
- });
- },
- createPattern: function() {
- storage.push({
- type: "function",
- name: "createPattern",
- 'arguments': arguments
- });
- },
- drawShape: function() {
-
- var shape = [];
-
- storage.push({
- type: "function",
- name: "drawShape",
- 'arguments': shape
- });
-
- return {
- moveTo: function() {
- shape.push({
- name: "moveTo",
- 'arguments': arguments
- });
- },
- lineTo: function() {
- shape.push({
- name: "lineTo",
- 'arguments': arguments
- });
- },
- arcTo: function() {
- shape.push({
- name: "arcTo",
- 'arguments': arguments
- });
- },
- bezierCurveTo: function() {
- shape.push({
- name: "bezierCurveTo",
- 'arguments': arguments
- });
- },
- quadraticCurveTo: function() {
- shape.push({
- name: "quadraticCurveTo",
- 'arguments': arguments
- });
- }
- };
-
- },
- drawImage: function () {
- storage.push({
- type: "function",
- name: "drawImage",
- 'arguments': arguments
- });
- },
- fillText: function () {
- storage.push({
- type: "function",
- name: "fillText",
- 'arguments': arguments
- });
- },
- setVariable: function (variable, value) {
- storage.push({
- type: "variable",
- name: variable,
- 'arguments': value
- });
- return value;
- }
- };
-}
-_html2canvas.Parse = function (images, options) {
- window.scroll(0,0);
-
- var element = (( options.elements === undefined ) ? document.body : options.elements[0]), // select body by default
- numDraws = 0,
- doc = element.ownerDocument,
- Util = _html2canvas.Util,
- support = Util.Support(options, doc),
- ignoreElementsRegExp = new RegExp("(" + options.ignoreElements + ")"),
- body = doc.body,
- getCSS = Util.getCSS,
- pseudoHide = "___html2canvas___pseudoelement",
- hidePseudoElements = doc.createElement('style');
-
- hidePseudoElements.innerHTML = '.' + pseudoHide + '-before:before { content: "" !important; display: none !important; }' +
- '.' + pseudoHide + '-after:after { content: "" !important; display: none !important; }';
-
- body.appendChild(hidePseudoElements);
-
- images = images || {};
-
- function documentWidth () {
- return Math.max(
- Math.max(doc.body.scrollWidth, doc.documentElement.scrollWidth),
- Math.max(doc.body.offsetWidth, doc.documentElement.offsetWidth),
- Math.max(doc.body.clientWidth, doc.documentElement.clientWidth)
- );
- }
-
- function documentHeight () {
- return Math.max(
- Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
- Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
- Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
- );
- }
-
- function getCSSInt(element, attribute) {
- var val = parseInt(getCSS(element, attribute), 10);
- return (isNaN(val)) ? 0 : val; // borders in old IE are throwing 'medium' for demo.html
- }
-
- function renderRect (ctx, x, y, w, h, bgcolor) {
- if (bgcolor !== "transparent"){
- ctx.setVariable("fillStyle", bgcolor);
- ctx.fillRect(x, y, w, h);
- numDraws+=1;
- }
- }
-
- function capitalize(m, p1, p2) {
- if (m.length > 0) {
- return p1 + p2.toUpperCase();
- }
- }
-
- function textTransform (text, transform) {
- switch(transform){
- case "lowercase":
- return text.toLowerCase();
- case "capitalize":
- return text.replace( /(^|\s|:|-|\(|\))([a-z])/g, capitalize);
- case "uppercase":
- return text.toUpperCase();
- default:
- return text;
- }
- }
-
- function noLetterSpacing(letter_spacing) {
- return (/^(normal|none|0px)$/.test(letter_spacing));
- }
-
- function drawText(currentText, x, y, ctx){
- if (currentText !== null && Util.trimText(currentText).length > 0) {
- ctx.fillText(currentText, x, y);
- numDraws+=1;
- }
- }
-
- function setTextVariables(ctx, el, text_decoration, color) {
- var align = false,
- bold = getCSS(el, "fontWeight"),
- family = getCSS(el, "fontFamily"),
- size = getCSS(el, "fontSize"),
- shadows = Util.parseTextShadows(getCSS(el, "textShadow"));
-
- switch(parseInt(bold, 10)){
- case 401:
- bold = "bold";
- break;
- case 400:
- bold = "normal";
- break;
- }
-
- ctx.setVariable("fillStyle", color);
- ctx.setVariable("font", [getCSS(el, "fontStyle"), getCSS(el, "fontVariant"), bold, size, family].join(" "));
- ctx.setVariable("textAlign", (align) ? "right" : "left");
-
- if (shadows.length) {
- // TODO: support multiple text shadows
- // apply the first text shadow
- ctx.setVariable("shadowColor", shadows[0].color);
- ctx.setVariable("shadowOffsetX", shadows[0].offsetX);
- ctx.setVariable("shadowOffsetY", shadows[0].offsetY);
- ctx.setVariable("shadowBlur", shadows[0].blur);
- }
-
- if (text_decoration !== "none"){
- return Util.Font(family, size, doc);
- }
- }
-
- function renderTextDecoration(ctx, text_decoration, bounds, metrics, color) {
- switch(text_decoration) {
- case "underline":
- // Draws a line at the baseline of the font
- // TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size
- renderRect(ctx, bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, color);
- break;
- case "overline":
- renderRect(ctx, bounds.left, Math.round(bounds.top), bounds.width, 1, color);
- break;
- case "line-through":
- // TODO try and find exact position for line-through
- renderRect(ctx, bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, color);
- break;
- }
- }
-
- function getTextBounds(state, text, textDecoration, isLast, transform) {
- var bounds;
- if (support.rangeBounds && !transform) {
- if (textDecoration !== "none" || Util.trimText(text).length !== 0) {
- bounds = textRangeBounds(text, state.node, state.textOffset);
- }
- state.textOffset += text.length;
- } else if (state.node && typeof state.node.nodeValue === "string" ){
- var newTextNode = (isLast) ? state.node.splitText(text.length) : null;
- bounds = textWrapperBounds(state.node, transform);
- state.node = newTextNode;
- }
- return bounds;
- }
-
- function textRangeBounds(text, textNode, textOffset) {
- var range = doc.createRange();
- range.setStart(textNode, textOffset);
- range.setEnd(textNode, textOffset + text.length);
- return range.getBoundingClientRect();
- }
-
- function textWrapperBounds(oldTextNode, transform) {
- var parent = oldTextNode.parentNode,
- wrapElement = doc.createElement('wrapper'),
- backupText = oldTextNode.cloneNode(true);
-
- wrapElement.appendChild(oldTextNode.cloneNode(true));
- parent.replaceChild(wrapElement, oldTextNode);
-
- var bounds = transform ? Util.OffsetBounds(wrapElement) : Util.Bounds(wrapElement);
- parent.replaceChild(backupText, wrapElement);
- return bounds;
- }
-
- function renderText(el, textNode, stack) {
- var ctx = stack.ctx,
- color = getCSS(el, "color"),
- textDecoration = getCSS(el, "textDecoration"),
- textAlign = getCSS(el, "textAlign"),
- metrics,
- textList,
- state = {
- node: textNode,
- textOffset: 0
- };
-
- if (Util.trimText(textNode.nodeValue).length > 0) {
- textNode.nodeValue = textTransform(textNode.nodeValue, getCSS(el, "textTransform"));
- textAlign = textAlign.replace(["-webkit-auto"],["auto"]);
-
- textList = (!options.letterRendering && /^(left|right|justify|auto)$/.test(textAlign) && noLetterSpacing(getCSS(el, "letterSpacing"))) ?
- textNode.nodeValue.split(/(\b| )/)
- : textNode.nodeValue.split("");
-
- metrics = setTextVariables(ctx, el, textDecoration, color);
-
- if (options.chinese) {
- textList.forEach(function(word, index) {
- if (/.*[\u4E00-\u9FA5].*$/.test(word)) {
- word = word.split("");
- word.unshift(index, 1);
- textList.splice.apply(textList, word);
- }
- });
- }
-
- textList.forEach(function(text, index) {
- var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1), stack.transform.matrix);
- if (bounds) {
- drawText(text, bounds.left, bounds.bottom, ctx);
- renderTextDecoration(ctx, textDecoration, bounds, metrics, color);
- }
- });
- }
- }
-
- function listPosition (element, val) {
- var boundElement = doc.createElement( "boundelement" ),
- originalType,
- bounds;
-
- boundElement.style.display = "inline";
-
- originalType = element.style.listStyleType;
- element.style.listStyleType = "none";
-
- boundElement.appendChild(doc.createTextNode(val));
-
- element.insertBefore(boundElement, element.firstChild);
-
- bounds = Util.Bounds(boundElement);
- element.removeChild(boundElement);
- element.style.listStyleType = originalType;
- return bounds;
- }
-
- function elementIndex(el) {
- var i = -1,
- count = 1,
- childs = el.parentNode.childNodes;
-
- if (el.parentNode) {
- while(childs[++i] !== el) {
- if (childs[i].nodeType === 1) {
- count++;
- }
- }
- return count;
- } else {
- return -1;
- }
- }
-
- function listItemText(element, type) {
- var currentIndex = elementIndex(element), text;
- switch(type){
- case "decimal":
- text = currentIndex;
- break;
- case "decimal-leading-zero":
- text = (currentIndex.toString().length === 1) ? currentIndex = "0" + currentIndex.toString() : currentIndex.toString();
- break;
- case "upper-roman":
- text = _html2canvas.Generate.ListRoman( currentIndex );
- break;
- case "lower-roman":
- text = _html2canvas.Generate.ListRoman( currentIndex ).toLowerCase();
- break;
- case "lower-alpha":
- text = _html2canvas.Generate.ListAlpha( currentIndex ).toLowerCase();
- break;
- case "upper-alpha":
- text = _html2canvas.Generate.ListAlpha( currentIndex );
- break;
- }
-
- return text + ". ";
- }
-
- function renderListItem(element, stack, elBounds) {
- var x,
- text,
- ctx = stack.ctx,
- type = getCSS(element, "listStyleType"),
- listBounds;
-
- if (/^(decimal|decimal-leading-zero|upper-alpha|upper-latin|upper-roman|lower-alpha|lower-greek|lower-latin|lower-roman)$/i.test(type)) {
- text = listItemText(element, type);
- listBounds = listPosition(element, text);
- setTextVariables(ctx, element, "none", getCSS(element, "color"));
-
- if (getCSS(element, "listStylePosition") === "inside") {
- ctx.setVariable("textAlign", "left");
- x = elBounds.left;
- } else {
- return;
- }
-
- drawText(text, x, listBounds.bottom, ctx);
- }
- }
-
- function loadImage (src){
- var img = images[src];
- return (img && img.succeeded === true) ? img.img : false;
- }
-
- function clipBounds(src, dst){
- var x = Math.max(src.left, dst.left),
- y = Math.max(src.top, dst.top),
- x2 = Math.min((src.left + src.width), (dst.left + dst.width)),
- y2 = Math.min((src.top + src.height), (dst.top + dst.height));
-
- return {
- left:x,
- top:y,
- width:x2-x,
- height:y2-y
- };
- }
-
- function setZ(element, stack, parentStack){
- var newContext,
- isPositioned = stack.cssPosition !== 'static',
- zIndex = isPositioned ? getCSS(element, 'zIndex') : 'auto',
- opacity = getCSS(element, 'opacity'),
- isFloated = getCSS(element, 'cssFloat') !== 'none';
-
- // https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context
- // When a new stacking context should be created:
- // the root element (HTML),
- // positioned (absolutely or relatively) with a z-index value other than "auto",
- // elements with an opacity value less than 1. (See the specification for opacity),
- // on mobile WebKit and Chrome 22+, position: fixed always creates a new stacking context, even when z-index is "auto" (See this post)
-
- stack.zIndex = newContext = h2czContext(zIndex);
- newContext.isPositioned = isPositioned;
- newContext.isFloated = isFloated;
- newContext.opacity = opacity;
- newContext.ownStacking = (zIndex !== 'auto' || opacity < 1);
-
- if (parentStack) {
- parentStack.zIndex.children.push(stack);
- }
- }
-
- function renderImage(ctx, element, image, bounds, borders) {
-
- var paddingLeft = getCSSInt(element, 'paddingLeft'),
- paddingTop = getCSSInt(element, 'paddingTop'),
- paddingRight = getCSSInt(element, 'paddingRight'),
- paddingBottom = getCSSInt(element, 'paddingBottom');
-
- drawImage(
- ctx,
- image,
- 0, //sx
- 0, //sy
- image.width, //sw
- image.height, //sh
- bounds.left + paddingLeft + borders[3].width, //dx
- bounds.top + paddingTop + borders[0].width, // dy
- bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight), //dw
- bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom) //dh
- );
- }
-
- function getBorderData(element) {
- return ["Top", "Right", "Bottom", "Left"].map(function(side) {
- return {
- width: getCSSInt(element, 'border' + side + 'Width'),
- color: getCSS(element, 'border' + side + 'Color')
- };
- });
- }
-
- function getBorderRadiusData(element) {
- return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) {
- return getCSS(element, 'border' + side + 'Radius');
- });
- }
-
- var getCurvePoints = (function(kappa) {
-
- return function(x, y, r1, r2) {
- var ox = (r1) * kappa, // control point offset horizontal
- oy = (r2) * kappa, // control point offset vertical
- xm = x + r1, // x-middle
- ym = y + r2; // y-middle
- return {
- topLeft: bezierCurve({
- x:x,
- y:ym
- }, {
- x:x,
- y:ym - oy
- }, {
- x:xm - ox,
- y:y
- }, {
- x:xm,
- y:y
- }),
- topRight: bezierCurve({
- x:x,
- y:y
- }, {
- x:x + ox,
- y:y
- }, {
- x:xm,
- y:ym - oy
- }, {
- x:xm,
- y:ym
- }),
- bottomRight: bezierCurve({
- x:xm,
- y:y
- }, {
- x:xm,
- y:y + oy
- }, {
- x:x + ox,
- y:ym
- }, {
- x:x,
- y:ym
- }),
- bottomLeft: bezierCurve({
- x:xm,
- y:ym
- }, {
- x:xm - ox,
- y:ym
- }, {
- x:x,
- y:y + oy
- }, {
- x:x,
- y:y
- })
- };
- };
- })(4 * ((Math.sqrt(2) - 1) / 3));
-
- function bezierCurve(start, startControl, endControl, end) {
-
- var lerp = function (a, b, t) {
- return {
- x:a.x + (b.x - a.x) * t,
- y:a.y + (b.y - a.y) * t
- };
- };
-
- return {
- start: start,
- startControl: startControl,
- endControl: endControl,
- end: end,
- subdivide: function(t) {
- var ab = lerp(start, startControl, t),
- bc = lerp(startControl, endControl, t),
- cd = lerp(endControl, end, t),
- abbc = lerp(ab, bc, t),
- bccd = lerp(bc, cd, t),
- dest = lerp(abbc, bccd, t);
- return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)];
- },
- curveTo: function(borderArgs) {
- borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]);
- },
- curveToReversed: function(borderArgs) {
- borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]);
- }
- };
- }
-
- function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) {
- if (radius1[0] > 0 || radius1[1] > 0) {
- borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]);
- corner1[0].curveTo(borderArgs);
- corner1[1].curveTo(borderArgs);
- } else {
- borderArgs.push(["line", x, y]);
- }
-
- if (radius2[0] > 0 || radius2[1] > 0) {
- borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]);
- }
- }
-
- function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) {
- var borderArgs = [];
-
- if (radius1[0] > 0 || radius1[1] > 0) {
- borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]);
- outer1[1].curveTo(borderArgs);
- } else {
- borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]);
- }
-
- if (radius2[0] > 0 || radius2[1] > 0) {
- borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]);
- outer2[0].curveTo(borderArgs);
- borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]);
- inner2[0].curveToReversed(borderArgs);
- } else {
- borderArgs.push([ "line", borderData.c2[0], borderData.c2[1]]);
- borderArgs.push([ "line", borderData.c3[0], borderData.c3[1]]);
- }
-
- if (radius1[0] > 0 || radius1[1] > 0) {
- borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]);
- inner1[1].curveToReversed(borderArgs);
- } else {
- borderArgs.push([ "line", borderData.c4[0], borderData.c4[1]]);
- }
-
- return borderArgs;
- }
-
- function calculateCurvePoints(bounds, borderRadius, borders) {
-
- var x = bounds.left,
- y = bounds.top,
- width = bounds.width,
- height = bounds.height,
-
- tlh = borderRadius[0][0],
- tlv = borderRadius[0][1],
- trh = borderRadius[1][0],
- trv = borderRadius[1][1],
- brh = borderRadius[2][0],
- brv = borderRadius[2][1],
- blh = borderRadius[3][0],
- blv = borderRadius[3][1],
-
- topWidth = width - trh,
- rightHeight = height - brv,
- bottomWidth = width - brh,
- leftHeight = height - blv;
-
- return {
- topLeftOuter: getCurvePoints(
- x,
- y,
- tlh,
- tlv
- ).topLeft.subdivide(0.5),
-
- topLeftInner: getCurvePoints(
- x + borders[3].width,
- y + borders[0].width,
- Math.max(0, tlh - borders[3].width),
- Math.max(0, tlv - borders[0].width)
- ).topLeft.subdivide(0.5),
-
- topRightOuter: getCurvePoints(
- x + topWidth,
- y,
- trh,
- trv
- ).topRight.subdivide(0.5),
-
- topRightInner: getCurvePoints(
- x + Math.min(topWidth, width + borders[3].width),
- y + borders[0].width,
- (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width,
- trv - borders[0].width
- ).topRight.subdivide(0.5),
-
- bottomRightOuter: getCurvePoints(
- x + bottomWidth,
- y + rightHeight,
- brh,
- brv
- ).bottomRight.subdivide(0.5),
-
- bottomRightInner: getCurvePoints(
- x + Math.min(bottomWidth, width + borders[3].width),
- y + Math.min(rightHeight, height + borders[0].width),
- Math.max(0, brh - borders[1].width),
- Math.max(0, brv - borders[2].width)
- ).bottomRight.subdivide(0.5),
-
- bottomLeftOuter: getCurvePoints(
- x,
- y + leftHeight,
- blh,
- blv
- ).bottomLeft.subdivide(0.5),
-
- bottomLeftInner: getCurvePoints(
- x + borders[3].width,
- y + leftHeight,
- Math.max(0, blh - borders[3].width),
- Math.max(0, blv - borders[2].width)
- ).bottomLeft.subdivide(0.5)
- };
- }
-
- function getBorderClip(element, borderPoints, borders, radius, bounds) {
- var backgroundClip = getCSS(element, 'backgroundClip'),
- borderArgs = [];
-
- switch(backgroundClip) {
- case "content-box":
- case "padding-box":
- parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width);
- parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width);
- parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width);
- parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width);
- break;
-
- default:
- parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top);
- parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top);
- parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height);
- parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height);
- break;
- }
-
- return borderArgs;
- }
-
- function parseBorders(element, bounds, borders){
- var x = bounds.left,
- y = bounds.top,
- width = bounds.width,
- height = bounds.height,
- borderSide,
- bx,
- by,
- bw,
- bh,
- borderArgs,
- // http://www.w3.org/TR/css3-background/#the-border-radius
- borderRadius = getBorderRadiusData(element),
- borderPoints = calculateCurvePoints(bounds, borderRadius, borders),
- borderData = {
- clip: getBorderClip(element, borderPoints, borders, borderRadius, bounds),
- borders: []
- };
-
- for (borderSide = 0; borderSide < 4; borderSide++) {
-
- if (borders[borderSide].width > 0) {
- bx = x;
- by = y;
- bw = width;
- bh = height - (borders[2].width);
-
- switch(borderSide) {
- case 0:
- // top border
- bh = borders[0].width;
-
- borderArgs = drawSide({
- c1: [bx, by],
- c2: [bx + bw, by],
- c3: [bx + bw - borders[1].width, by + bh],
- c4: [bx + borders[3].width, by + bh]
- }, borderRadius[0], borderRadius[1],
- borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner);
- break;
- case 1:
- // right border
- bx = x + width - (borders[1].width);
- bw = borders[1].width;
-
- borderArgs = drawSide({
- c1: [bx + bw, by],
- c2: [bx + bw, by + bh + borders[2].width],
- c3: [bx, by + bh],
- c4: [bx, by + borders[0].width]
- }, borderRadius[1], borderRadius[2],
- borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner);
- break;
- case 2:
- // bottom border
- by = (by + height) - (borders[2].width);
- bh = borders[2].width;
-
- borderArgs = drawSide({
- c1: [bx + bw, by + bh],
- c2: [bx, by + bh],
- c3: [bx + borders[3].width, by],
- c4: [bx + bw - borders[3].width, by]
- }, borderRadius[2], borderRadius[3],
- borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner);
- break;
- case 3:
- // left border
- bw = borders[3].width;
-
- borderArgs = drawSide({
- c1: [bx, by + bh + borders[2].width],
- c2: [bx, by],
- c3: [bx + bw, by + borders[0].width],
- c4: [bx + bw, by + bh]
- }, borderRadius[3], borderRadius[0],
- borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner);
- break;
- }
-
- borderData.borders.push({
- args: borderArgs,
- color: borders[borderSide].color
- });
-
- }
- }
-
- return borderData;
- }
-
- function createShape(ctx, args) {
- var shape = ctx.drawShape();
- args.forEach(function(border, index) {
- shape[(index === 0) ? "moveTo" : border[0] + "To" ].apply(null, border.slice(1));
- });
- return shape;
- }
-
- function renderBorders(ctx, borderArgs, color) {
- if (color !== "transparent") {
- ctx.setVariable( "fillStyle", color);
- createShape(ctx, borderArgs);
- ctx.fill();
- numDraws+=1;
- }
- }
-
- function renderFormValue (el, bounds, stack){
-
- var valueWrap = doc.createElement('valuewrap'),
- cssPropertyArray = ['lineHeight','textAlign','fontFamily','color','fontSize','paddingLeft','paddingTop','width','height','border','borderLeftWidth','borderTopWidth'],
- textValue,
- textNode;
-
- cssPropertyArray.forEach(function(property) {
- try {
- valueWrap.style[property] = getCSS(el, property);
- } catch(e) {
- // Older IE has issues with "border"
- Util.log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
- }
- });
-
- valueWrap.style.borderColor = "black";
- valueWrap.style.borderStyle = "solid";
- valueWrap.style.display = "block";
- valueWrap.style.position = "absolute";
-
- if (/^(submit|reset|button|text|password)$/.test(el.type) || el.nodeName === "SELECT"){
- valueWrap.style.lineHeight = getCSS(el, "height");
- }
-
- valueWrap.style.top = bounds.top + "px";
- valueWrap.style.left = bounds.left + "px";
-
- textValue = (el.nodeName === "SELECT") ? (el.options[el.selectedIndex] || 0).text : el.value;
- if(!textValue) {
- textValue = el.placeholder;
- }
-
- textNode = doc.createTextNode(textValue);
-
- valueWrap.appendChild(textNode);
- body.appendChild(valueWrap);
-
- renderText(el, textNode, stack);
- body.removeChild(valueWrap);
- }
-
- function drawImage (ctx) {
- ctx.drawImage.apply(ctx, Array.prototype.slice.call(arguments, 1));
- numDraws+=1;
- }
-
- function getPseudoElement(el, which) {
- var elStyle = window.getComputedStyle(el, which);
- if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content" || elStyle.display === "none") {
- return;
- }
- var content = elStyle.content + '',
- first = content.substr( 0, 1 );
- //strips quotes
- if(first === content.substr( content.length - 1 ) && first.match(/'|"/)) {
- content = content.substr( 1, content.length - 2 );
- }
-
- var isImage = content.substr( 0, 3 ) === 'url',
- elps = document.createElement( isImage ? 'img' : 'span' );
-
- elps.className = pseudoHide + "-before " + pseudoHide + "-after";
-
- Object.keys(elStyle).filter(indexedProperty).forEach(function(prop) {
- // Prevent assigning of read only CSS Rules, ex. length, parentRule
- try {
- elps.style[prop] = elStyle[prop];
- } catch (e) {
- Util.log(['Tried to assign readonly property ', prop, 'Error:', e]);
- }
- });
-
- if(isImage) {
- elps.src = Util.parseBackgroundImage(content)[0].args[0];
- } else {
- elps.innerHTML = content;
- }
- return elps;
- }
-
- function indexedProperty(property) {
- return (isNaN(window.parseInt(property, 10)));
- }
-
- function injectPseudoElements(el, stack) {
- var before = getPseudoElement(el, ':before'),
- after = getPseudoElement(el, ':after');
- if(!before && !after) {
- return;
- }
-
- if(before) {
- el.className += " " + pseudoHide + "-before";
- el.parentNode.insertBefore(before, el);
- parseElement(before, stack, true);
- el.parentNode.removeChild(before);
- el.className = el.className.replace(pseudoHide + "-before", "").trim();
- }
-
- if (after) {
- el.className += " " + pseudoHide + "-after";
- el.appendChild(after);
- parseElement(after, stack, true);
- el.removeChild(after);
- el.className = el.className.replace(pseudoHide + "-after", "").trim();
- }
-
- }
-
- function renderBackgroundRepeat(ctx, image, backgroundPosition, bounds) {
- var offsetX = Math.round(bounds.left + backgroundPosition.left),
- offsetY = Math.round(bounds.top + backgroundPosition.top);
-
- ctx.createPattern(image);
- ctx.translate(offsetX, offsetY);
- ctx.fill();
- ctx.translate(-offsetX, -offsetY);
- }
-
- function backgroundRepeatShape(ctx, image, backgroundPosition, bounds, left, top, width, height) {
- var args = [];
- args.push(["line", Math.round(left), Math.round(top)]);
- args.push(["line", Math.round(left + width), Math.round(top)]);
- args.push(["line", Math.round(left + width), Math.round(height + top)]);
- args.push(["line", Math.round(left), Math.round(height + top)]);
- createShape(ctx, args);
- ctx.save();
- ctx.clip();
- renderBackgroundRepeat(ctx, image, backgroundPosition, bounds);
- ctx.restore();
- }
-
- function renderBackgroundColor(ctx, backgroundBounds, bgcolor) {
- renderRect(
- ctx,
- backgroundBounds.left,
- backgroundBounds.top,
- backgroundBounds.width,
- backgroundBounds.height,
- bgcolor
- );
- }
-
- function renderBackgroundRepeating(el, bounds, ctx, image, imageIndex) {
- var backgroundSize = Util.BackgroundSize(el, bounds, image, imageIndex),
- backgroundPosition = Util.BackgroundPosition(el, bounds, image, imageIndex, backgroundSize),
- backgroundRepeat = getCSS(el, "backgroundRepeat").split(",").map(Util.trimText);
-
- image = resizeImage(image, backgroundSize);
-
- backgroundRepeat = backgroundRepeat[imageIndex] || backgroundRepeat[0];
-
- switch (backgroundRepeat) {
- case "repeat-x":
- backgroundRepeatShape(ctx, image, backgroundPosition, bounds,
- bounds.left, bounds.top + backgroundPosition.top, 99999, image.height);
- break;
-
- case "repeat-y":
- backgroundRepeatShape(ctx, image, backgroundPosition, bounds,
- bounds.left + backgroundPosition.left, bounds.top, image.width, 99999);
- break;
-
- case "no-repeat":
- backgroundRepeatShape(ctx, image, backgroundPosition, bounds,
- bounds.left + backgroundPosition.left, bounds.top + backgroundPosition.top, image.width, image.height);
- break;
-
- default:
- renderBackgroundRepeat(ctx, image, backgroundPosition, {
- top: bounds.top,
- left: bounds.left,
- width: image.width,
- height: image.height
- });
- break;
- }
- }
-
- function renderBackgroundImage(element, bounds, ctx) {
- var backgroundImage = getCSS(element, "backgroundImage"),
- backgroundImages = Util.parseBackgroundImage(backgroundImage),
- image,
- imageIndex = backgroundImages.length;
-
- while(imageIndex--) {
- backgroundImage = backgroundImages[imageIndex];
-
- if (!backgroundImage.args || backgroundImage.args.length === 0) {
- continue;
- }
-
- var key = backgroundImage.method === 'url' ?
- backgroundImage.args[0] :
- backgroundImage.value;
-
- image = loadImage(key);
-
- // TODO add support for background-origin
- if (image) {
- renderBackgroundRepeating(element, bounds, ctx, image, imageIndex);
- } else {
- Util.log("html2canvas: Error loading background:", backgroundImage);
- }
- }
- }
-
- function resizeImage(image, bounds) {
- if(image.width === bounds.width && image.height === bounds.height) {
- return image;
- }
-
- var ctx, canvas = doc.createElement('canvas');
- canvas.width = bounds.width;
- canvas.height = bounds.height;
- ctx = canvas.getContext("2d");
- drawImage(ctx, image, 0, 0, image.width, image.height, 0, 0, bounds.width, bounds.height );
- return canvas;
- }
-
- function setOpacity(ctx, element, parentStack) {
- return ctx.setVariable("globalAlpha", getCSS(element, "opacity") * ((parentStack) ? parentStack.opacity : 1));
- }
-
- function removePx(str) {
- return str.replace("px", "");
- }
-
- var transformRegExp = /(matrix)\((.+)\)/;
-
- function getTransform(element, parentStack) {
- var transform = getCSS(element, "transform") || getCSS(element, "-webkit-transform") || getCSS(element, "-moz-transform") || getCSS(element, "-ms-transform") || getCSS(element, "-o-transform");
- var transformOrigin = getCSS(element, "transform-origin") || getCSS(element, "-webkit-transform-origin") || getCSS(element, "-moz-transform-origin") || getCSS(element, "-ms-transform-origin") || getCSS(element, "-o-transform-origin") || "0px 0px";
-
- transformOrigin = transformOrigin.split(" ").map(removePx).map(Util.asFloat);
-
- var matrix;
- if (transform && transform !== "none") {
- var match = transform.match(transformRegExp);
- if (match) {
- switch(match[1]) {
- case "matrix":
- matrix = match[2].split(",").map(Util.trimText).map(Util.asFloat);
- break;
- }
- }
- }
-
- return {
- origin: transformOrigin,
- matrix: matrix
- };
- }
-
- function createStack(element, parentStack, bounds, transform) {
- var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
- stack = {
- ctx: ctx,
- opacity: setOpacity(ctx, element, parentStack),
- cssPosition: getCSS(element, "position"),
- borders: getBorderData(element),
- transform: transform,
- clip: (parentStack && parentStack.clip) ? Util.Extend( {}, parentStack.clip ) : null
- };
-
- setZ(element, stack, parentStack);
-
- // TODO correct overflow for absolute content residing under a static position
- if (options.useOverflow === true && /(hidden|scroll|auto)/.test(getCSS(element, "overflow")) === true && /(BODY)/i.test(element.nodeName) === false){
- stack.clip = (stack.clip) ? clipBounds(stack.clip, bounds) : bounds;
- }
-
- return stack;
- }
-
- function getBackgroundBounds(borders, bounds, clip) {
- var backgroundBounds = {
- left: bounds.left + borders[3].width,
- top: bounds.top + borders[0].width,
- width: bounds.width - (borders[1].width + borders[3].width),
- height: bounds.height - (borders[0].width + borders[2].width)
- };
-
- if (clip) {
- backgroundBounds = clipBounds(backgroundBounds, clip);
- }
-
- return backgroundBounds;
- }
-
- function getBounds(element, transform) {
- var bounds = (transform.matrix) ? Util.OffsetBounds(element) : Util.Bounds(element);
- transform.origin[0] += bounds.left;
- transform.origin[1] += bounds.top;
- return bounds;
- }
-
- function renderElement(element, parentStack, pseudoElement, ignoreBackground) {
- var transform = getTransform(element, parentStack),
- bounds = getBounds(element, transform),
- image,
- stack = createStack(element, parentStack, bounds, transform),
- borders = stack.borders,
- ctx = stack.ctx,
- backgroundBounds = getBackgroundBounds(borders, bounds, stack.clip),
- borderData = parseBorders(element, bounds, borders),
- backgroundColor = (ignoreElementsRegExp.test(element.nodeName)) ? "#efefef" : getCSS(element, "backgroundColor");
-
-
- createShape(ctx, borderData.clip);
-
- ctx.save();
- ctx.clip();
-
- if (backgroundBounds.height > 0 && backgroundBounds.width > 0 && !ignoreBackground) {
- renderBackgroundColor(ctx, bounds, backgroundColor);
- renderBackgroundImage(element, backgroundBounds, ctx);
- } else if (ignoreBackground) {
- stack.backgroundColor = backgroundColor;
- }
-
- ctx.restore();
-
- borderData.borders.forEach(function(border) {
- renderBorders(ctx, border.args, border.color);
- });
-
- if (!pseudoElement) {
- injectPseudoElements(element, stack);
- }
-
- switch(element.nodeName){
- case "IMG":
- if ((image = loadImage(element.getAttribute('src')))) {
- renderImage(ctx, element, image, bounds, borders);
- } else {
- Util.log("html2canvas: Error loading :" + element.getAttribute('src'));
- }
- break;
- case "INPUT":
- // TODO add all relevant type's, i.e. HTML5 new stuff
- // todo add support for placeholder attribute for browsers which support it
- if (/^(text|url|email|submit|button|reset)$/.test(element.type) && (element.value || element.placeholder || "").length > 0){
- renderFormValue(element, bounds, stack);
- }
- break;
- case "TEXTAREA":
- if ((element.value || element.placeholder || "").length > 0){
- renderFormValue(element, bounds, stack);
- }
- break;
- case "SELECT":
- if ((element.options||element.placeholder || "").length > 0){
- renderFormValue(element, bounds, stack);
- }
- break;
- case "LI":
- renderListItem(element, stack, backgroundBounds);
- break;
- case "CANVAS":
- renderImage(ctx, element, element, bounds, borders);
- break;
- }
-
- return stack;
- }
-
- function isElementVisible(element) {
- return (getCSS(element, 'display') !== "none" && getCSS(element, 'visibility') !== "hidden" && !element.hasAttribute("data-html2canvas-ignore"));
- }
-
- function parseElement (element, stack, pseudoElement) {
- if (isElementVisible(element)) {
- stack = renderElement(element, stack, pseudoElement, false) || stack;
- if (!ignoreElementsRegExp.test(element.nodeName)) {
- parseChildren(element, stack, pseudoElement);
- }
- }
- }
-
- function parseChildren(element, stack, pseudoElement) {
- Util.Children(element).forEach(function(node) {
- if (node.nodeType === node.ELEMENT_NODE) {
- parseElement(node, stack, pseudoElement);
- } else if (node.nodeType === node.TEXT_NODE) {
- renderText(element, node, stack);
- }
- });
- }
-
- function init() {
- var background = getCSS(document.documentElement, "backgroundColor"),
- transparentBackground = (Util.isTransparent(background) && element === document.body),
- stack = renderElement(element, null, false, transparentBackground);
- parseChildren(element, stack);
-
- if (transparentBackground) {
- background = stack.backgroundColor;
- }
-
- body.removeChild(hidePseudoElements);
- return {
- backgroundColor: background,
- stack: stack
- };
- }
-
- return init();
-};
-
-function h2czContext(zindex) {
- return {
- zindex: zindex,
- children: []
- };
-}
-
-_html2canvas.Preload = function( options ) {
-
- var images = {
- numLoaded: 0, // also failed are counted here
- numFailed: 0,
- numTotal: 0,
- cleanupDone: false
- },
- pageOrigin,
- Util = _html2canvas.Util,
- methods,
- i,
- count = 0,
- element = options.elements[0] || document.body,
- doc = element.ownerDocument,
- domImages = element.getElementsByTagName('img'), // Fetch images of the present element only
- imgLen = domImages.length,
- link = doc.createElement("a"),
- supportCORS = (function( img ){
- return (img.crossOrigin !== undefined);
- })(new Image()),
- timeoutTimer;
-
- link.href = window.location.href;
- pageOrigin = link.protocol + link.host;
-
- function isSameOrigin(url){
- link.href = url;
- link.href = link.href; // YES, BELIEVE IT OR NOT, that is required for IE9 - http://jsfiddle.net/niklasvh/2e48b/
- var origin = link.protocol + link.host;
- return (origin === pageOrigin);
- }
-
- function start(){
- Util.log("html2canvas: start: images: " + images.numLoaded + " / " + images.numTotal + " (failed: " + images.numFailed + ")");
- if (!images.firstRun && images.numLoaded >= images.numTotal){
- Util.log("Finished loading images: # " + images.numTotal + " (failed: " + images.numFailed + ")");
-
- if (typeof options.complete === "function"){
- options.complete(images);
- }
-
- }
- }
-
- // TODO modify proxy to serve images with CORS enabled, where available
- function proxyGetImage(url, img, imageObj){
- var callback_name,
- scriptUrl = options.proxy,
- script;
-
- link.href = url;
- url = link.href; // work around for pages with base href="" set - WARNING: this may change the url
-
- callback_name = 'html2canvas_' + (count++);
- imageObj.callbackname = callback_name;
-
- if (scriptUrl.indexOf("?") > -1) {
- scriptUrl += "&";
- } else {
- scriptUrl += "?";
- }
- scriptUrl += 'url=' + encodeURIComponent(url) + '&callback=' + callback_name;
- script = doc.createElement("script");
-
- window[callback_name] = function(a){
- if (a.substring(0,6) === "error:"){
- imageObj.succeeded = false;
- images.numLoaded++;
- images.numFailed++;
- start();
- } else {
- setImageLoadHandlers(img, imageObj);
- img.src = a;
- }
- window[callback_name] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9)
- try {
- delete window[callback_name]; // for all browser that support this
- } catch(ex) {}
- script.parentNode.removeChild(script);
- script = null;
- delete imageObj.script;
- delete imageObj.callbackname;
- };
-
- script.setAttribute("type", "text/javascript");
- script.setAttribute("src", scriptUrl);
- imageObj.script = script;
- window.document.body.appendChild(script);
-
- }
-
- function loadPseudoElement(element, type) {
- var style = window.getComputedStyle(element, type),
- content = style.content;
- if (content.substr(0, 3) === 'url') {
- methods.loadImage(_html2canvas.Util.parseBackgroundImage(content)[0].args[0]);
- }
- loadBackgroundImages(style.backgroundImage, element);
- }
-
- function loadPseudoElementImages(element) {
- loadPseudoElement(element, ":before");
- loadPseudoElement(element, ":after");
- }
-
- function loadGradientImage(backgroundImage, bounds) {
- var img = _html2canvas.Generate.Gradient(backgroundImage, bounds);
-
- if (img !== undefined){
- images[backgroundImage] = {
- img: img,
- succeeded: true
- };
- images.numTotal++;
- images.numLoaded++;
- start();
- }
- }
-
- function invalidBackgrounds(background_image) {
- return (background_image && background_image.method && background_image.args && background_image.args.length > 0 );
- }
-
- function loadBackgroundImages(background_image, el) {
- var bounds;
-
- _html2canvas.Util.parseBackgroundImage(background_image).filter(invalidBackgrounds).forEach(function(background_image) {
- if (background_image.method === 'url') {
- methods.loadImage(background_image.args[0]);
- } else if(background_image.method.match(/\-?gradient$/)) {
- if(bounds === undefined) {
- bounds = _html2canvas.Util.Bounds(el);
- }
- loadGradientImage(background_image.value, bounds);
- }
- });
- }
-
- function getImages (el) {
- var elNodeType = false;
-
- // Firefox fails with permission denied on pages with iframes
- try {
- Util.Children(el).forEach(getImages);
- }
- catch( e ) {}
-
- try {
- elNodeType = el.nodeType;
- } catch (ex) {
- elNodeType = false;
- Util.log("html2canvas: failed to access some element's nodeType - Exception: " + ex.message);
- }
-
- if (elNodeType === 1 || elNodeType === undefined) {
- loadPseudoElementImages(el);
- try {
- loadBackgroundImages(Util.getCSS(el, 'backgroundImage'), el);
- } catch(e) {
- Util.log("html2canvas: failed to get background-image - Exception: " + e.message);
- }
- loadBackgroundImages(el);
- }
- }
-
- function setImageLoadHandlers(img, imageObj) {
- img.onload = function() {
- if ( imageObj.timer !== undefined ) {
- // CORS succeeded
- window.clearTimeout( imageObj.timer );
- }
-
- images.numLoaded++;
- imageObj.succeeded = true;
- img.onerror = img.onload = null;
- start();
- };
- img.onerror = function() {
- if (img.crossOrigin === "anonymous") {
- // CORS failed
- window.clearTimeout( imageObj.timer );
-
- // let's try with proxy instead
- if ( options.proxy ) {
- var src = img.src;
- img = new Image();
- imageObj.img = img;
- img.src = src;
-
- proxyGetImage( img.src, img, imageObj );
- return;
- }
- }
-
- images.numLoaded++;
- images.numFailed++;
- imageObj.succeeded = false;
- img.onerror = img.onload = null;
- start();
- };
- }
-
- methods = {
- loadImage: function( src ) {
- var img, imageObj;
- if ( src && images[src] === undefined ) {
- img = new Image();
- if ( src.match(/data:image\/.*;base64,/i) ) {
- img.src = src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, '');
- imageObj = images[src] = {
- img: img
- };
- images.numTotal++;
- setImageLoadHandlers(img, imageObj);
- } else if ( isSameOrigin( src ) || options.allowTaint === true ) {
- imageObj = images[src] = {
- img: img
- };
- images.numTotal++;
- setImageLoadHandlers(img, imageObj);
- img.src = src;
- } else if ( supportCORS && !options.allowTaint && options.useCORS ) {
- // attempt to load with CORS
-
- img.crossOrigin = "anonymous";
- imageObj = images[src] = {
- img: img
- };
- images.numTotal++;
- setImageLoadHandlers(img, imageObj);
- img.src = src;
- } else if ( options.proxy ) {
- imageObj = images[src] = {
- img: img
- };
- images.numTotal++;
- proxyGetImage( src, img, imageObj );
- }
- }
-
- },
- cleanupDOM: function(cause) {
- var img, src;
- if (!images.cleanupDone) {
- if (cause && typeof cause === "string") {
- Util.log("html2canvas: Cleanup because: " + cause);
- } else {
- Util.log("html2canvas: Cleanup after timeout: " + options.timeout + " ms.");
- }
-
- for (src in images) {
- if (images.hasOwnProperty(src)) {
- img = images[src];
- if (typeof img === "object" && img.callbackname && img.succeeded === undefined) {
- // cancel proxy image request
- window[img.callbackname] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9)
- try {
- delete window[img.callbackname]; // for all browser that support this
- } catch(ex) {}
- if (img.script && img.script.parentNode) {
- img.script.setAttribute("src", "about:blank"); // try to cancel running request
- img.script.parentNode.removeChild(img.script);
- }
- images.numLoaded++;
- images.numFailed++;
- Util.log("html2canvas: Cleaned up failed img: '" + src + "' Steps: " + images.numLoaded + " / " + images.numTotal);
- }
- }
- }
-
- // cancel any pending requests
- if(window.stop !== undefined) {
- window.stop();
- } else if(document.execCommand !== undefined) {
- document.execCommand("Stop", false);
- }
- if (document.close !== undefined) {
- document.close();
- }
- images.cleanupDone = true;
- if (!(cause && typeof cause === "string")) {
- start();
- }
- }
- },
-
- renderingDone: function() {
- if (timeoutTimer) {
- window.clearTimeout(timeoutTimer);
- }
- }
- };
-
- if (options.timeout > 0) {
- timeoutTimer = window.setTimeout(methods.cleanupDOM, options.timeout);
- }
-
- Util.log('html2canvas: Preload starts: finding background-images');
- images.firstRun = true;
-
- getImages(element);
-
- Util.log('html2canvas: Preload: Finding images');
- // load images
- for (i = 0; i < imgLen; i+=1){
- methods.loadImage( domImages[i].getAttribute( "src" ) );
- }
-
- images.firstRun = false;
- Util.log('html2canvas: Preload: Done.');
- if (images.numTotal === images.numLoaded) {
- start();
- }
-
- return methods;
-};
-
-_html2canvas.Renderer = function(parseQueue, options){
-
- // http://www.w3.org/TR/CSS21/zindex.html
- function createRenderQueue(parseQueue) {
- var queue = [],
- rootContext;
-
- rootContext = (function buildStackingContext(rootNode) {
- var rootContext = {};
- function insert(context, node, specialParent) {
- var zi = (node.zIndex.zindex === 'auto') ? 0 : Number(node.zIndex.zindex),
- contextForChildren = context, // the stacking context for children
- isPositioned = node.zIndex.isPositioned,
- isFloated = node.zIndex.isFloated,
- stub = {node: node},
- childrenDest = specialParent; // where children without z-index should be pushed into
-
- if (node.zIndex.ownStacking) {
- // '!' comes before numbers in sorted array
- contextForChildren = stub.context = { '!': [{node:node, children: []}]};
- childrenDest = undefined;
- } else if (isPositioned || isFloated) {
- childrenDest = stub.children = [];
- }
-
- if (zi === 0 && specialParent) {
- specialParent.push(stub);
- } else {
- if (!context[zi]) { context[zi] = []; }
- context[zi].push(stub);
- }
-
- node.zIndex.children.forEach(function(childNode) {
- insert(contextForChildren, childNode, childrenDest);
- });
- }
- insert(rootContext, rootNode);
- return rootContext;
- })(parseQueue);
-
- function sortZ(context) {
- Object.keys(context).sort().forEach(function(zi) {
- var nonPositioned = [],
- floated = [],
- positioned = [],
- list = [];
-
- // positioned after static
- context[zi].forEach(function(v) {
- if (v.node.zIndex.isPositioned || v.node.zIndex.opacity < 1) {
- // http://www.w3.org/TR/css3-color/#transparency
- // non-positioned element with opactiy < 1 should be stacked as if it were a positioned element with ‘z-index: 0’ and ‘opacity: 1’.
- positioned.push(v);
- } else if (v.node.zIndex.isFloated) {
- floated.push(v);
- } else {
- nonPositioned.push(v);
- }
- });
-
- (function walk(arr) {
- arr.forEach(function(v) {
- list.push(v);
- if (v.children) { walk(v.children); }
- });
- })(nonPositioned.concat(floated, positioned));
-
- list.forEach(function(v) {
- if (v.context) {
- sortZ(v.context);
- } else {
- queue.push(v.node);
- }
- });
- });
- }
-
- sortZ(rootContext);
-
- return queue;
- }
-
- function getRenderer(rendererName) {
- var renderer;
-
- if (typeof options.renderer === "string" && _html2canvas.Renderer[rendererName] !== undefined) {
- renderer = _html2canvas.Renderer[rendererName](options);
- } else if (typeof rendererName === "function") {
- renderer = rendererName(options);
- } else {
- throw new Error("Unknown renderer");
- }
-
- if ( typeof renderer !== "function" ) {
- throw new Error("Invalid renderer defined");
- }
- return renderer;
- }
-
- return getRenderer(options.renderer)(parseQueue, options, document, createRenderQueue(parseQueue.stack), _html2canvas);
-};
-
-_html2canvas.Util.Support = function (options, doc) {
-
- function supportSVGRendering() {
- var img = new Image(),
- canvas = doc.createElement("canvas"),
- ctx = (canvas.getContext === undefined) ? false : canvas.getContext("2d");
- if (ctx === false) {
- return false;
- }
- canvas.width = canvas.height = 10;
- img.src = [
- "data:image/svg+xml,",
- "",
- "",
- "",
- "sup",
- "
",
- " ",
- " "
- ].join("");
- try {
- ctx.drawImage(img, 0, 0);
- canvas.toDataURL();
- } catch(e) {
- return false;
- }
- _html2canvas.Util.log('html2canvas: Parse: SVG powered rendering available');
- return true;
- }
-
- // Test whether we can use ranges to measure bounding boxes
- // Opera doesn't provide valid bounds.height/bottom even though it supports the method.
-
- function supportRangeBounds() {
- var r, testElement, rangeBounds, rangeHeight, support = false;
-
- if (doc.createRange) {
- r = doc.createRange();
- if (r.getBoundingClientRect) {
- testElement = doc.createElement('boundtest');
- testElement.style.height = "123px";
- testElement.style.display = "block";
- doc.body.appendChild(testElement);
-
- r.selectNode(testElement);
- rangeBounds = r.getBoundingClientRect();
- rangeHeight = rangeBounds.height;
-
- if (rangeHeight === 123) {
- support = true;
- }
- doc.body.removeChild(testElement);
- }
- }
-
- return support;
- }
-
- return {
- rangeBounds: supportRangeBounds(),
- svgRendering: options.svgRendering && supportSVGRendering()
- };
-};
-window.html2canvas = function(elements, opts) {
- elements = (elements.length) ? elements : [elements];
- var queue,
- canvas,
- options = {
- // general
- logging: false,
- elements: elements,
- background: "#fff",
-
- // preload options
- proxy: null,
- timeout: 0, // no timeout
- useCORS: false, // try to load images as CORS (where available), before falling back to proxy
- allowTaint: false, // whether to allow images to taint the canvas, won't need proxy if set to true
-
- // parse options
- svgRendering: false, // use svg powered rendering where available (FF11+)
- ignoreElements: "IFRAME|OBJECT|PARAM",
- useOverflow: true,
- letterRendering: false,
- chinese: false,
-
- // render options
-
- width: null,
- height: null,
- taintTest: true, // do a taint test with all images before applying to canvas
- renderer: "Canvas"
- };
-
- options = _html2canvas.Util.Extend(opts, options);
-
- _html2canvas.logging = options.logging;
- options.complete = function( images ) {
-
- if (typeof options.onpreloaded === "function") {
- if ( options.onpreloaded( images ) === false ) {
- return;
- }
- }
- queue = _html2canvas.Parse( images, options );
-
- if (typeof options.onparsed === "function") {
- if ( options.onparsed( queue ) === false ) {
- return;
- }
- }
-
- canvas = _html2canvas.Renderer( queue, options );
-
- if (typeof options.onrendered === "function") {
- options.onrendered( canvas );
- }
-
-
- };
-
- // for pages without images, we still want this to be async, i.e. return methods before executing
- window.setTimeout( function(){
- _html2canvas.Preload( options );
- }, 0 );
-
- return {
- render: function( queue, opts ) {
- return _html2canvas.Renderer( queue, _html2canvas.Util.Extend(opts, options) );
- },
- parse: function( images, opts ) {
- return _html2canvas.Parse( images, _html2canvas.Util.Extend(opts, options) );
- },
- preload: function( opts ) {
- return _html2canvas.Preload( _html2canvas.Util.Extend(opts, options) );
- },
- log: _html2canvas.Util.log
- };
-};
-
-window.html2canvas.log = _html2canvas.Util.log; // for renderers
-window.html2canvas.Renderer = {
- Canvas: undefined // We are assuming this will be used
-};
-_html2canvas.Renderer.Canvas = function(options) {
- options = options || {};
-
- var doc = document,
- safeImages = [],
- testCanvas = document.createElement("canvas"),
- testctx = testCanvas.getContext("2d"),
- Util = _html2canvas.Util,
- canvas = options.canvas || doc.createElement('canvas');
-
- function createShape(ctx, args) {
- ctx.beginPath();
- args.forEach(function(arg) {
- ctx[arg.name].apply(ctx, arg['arguments']);
- });
- ctx.closePath();
- }
-
- function safeImage(item) {
- if (safeImages.indexOf(item['arguments'][0].src ) === -1) {
- testctx.drawImage(item['arguments'][0], 0, 0);
- try {
- testctx.getImageData(0, 0, 1, 1);
- } catch(e) {
- testCanvas = doc.createElement("canvas");
- testctx = testCanvas.getContext("2d");
- return false;
- }
- safeImages.push(item['arguments'][0].src);
- }
- return true;
- }
-
- function renderItem(ctx, item) {
- switch(item.type){
- case "variable":
- ctx[item.name] = item['arguments'];
- break;
- case "function":
- switch(item.name) {
- case "createPattern":
- if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
- try {
- ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
- }
- catch(e) {
- Util.log("html2canvas: Renderer: Error creating pattern", e.message);
- }
- }
- break;
- case "drawShape":
- createShape(ctx, item['arguments']);
- break;
- case "drawImage":
- if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
- if (!options.taintTest || (options.taintTest && safeImage(item))) {
- ctx.drawImage.apply( ctx, item['arguments'] );
- }
- }
- break;
- default:
- ctx[item.name].apply(ctx, item['arguments']);
- }
- break;
- }
- }
-
- return function(parsedData, options, document, queue, _html2canvas) {
- var ctx = canvas.getContext("2d"),
- newCanvas,
- bounds,
- fstyle,
- zStack = parsedData.stack;
-
- canvas.width = canvas.style.width = options.width || zStack.ctx.width;
- canvas.height = canvas.style.height = options.height || zStack.ctx.height;
-
- fstyle = ctx.fillStyle;
- ctx.fillStyle = (Util.isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = fstyle;
-
- queue.forEach(function(storageContext) {
- // set common settings for canvas
- ctx.textBaseline = "bottom";
- ctx.save();
-
- if (storageContext.transform.matrix) {
- ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]);
- ctx.transform.apply(ctx, storageContext.transform.matrix);
- ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]);
- }
-
- if (storageContext.clip){
- ctx.beginPath();
- ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
- ctx.clip();
- }
-
- if (storageContext.ctx.storage) {
- storageContext.ctx.storage.forEach(function(item) {
- renderItem(ctx, item);
- });
- }
-
- ctx.restore();
- });
-
- Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj");
-
- if (options.elements.length === 1) {
- if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
- // crop image to the bounds of selected (single) element
- bounds = _html2canvas.Util.Bounds(options.elements[0]);
- newCanvas = document.createElement('canvas');
- newCanvas.width = Math.ceil(bounds.width);
- newCanvas.height = Math.ceil(bounds.height);
- ctx = newCanvas.getContext("2d");
-
- ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
- canvas = null;
- return newCanvas;
- }
- }
-
- return canvas;
- };
-};
-})(window,document);
\ No newline at end of file
diff --git a/vendor/jquery-ui/css/jquery-ui-1.12.1.min.css b/vendor/jquery-ui/css/jquery-ui-1.12.1.min.css
deleted file mode 100644
index 776e259..0000000
--- a/vendor/jquery-ui/css/jquery-ui-1.12.1.min.css
+++ /dev/null
@@ -1,7 +0,0 @@
-/*! jQuery UI - v1.12.1 - 2016-09-14
-* http://jqueryui.com
-* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css
-* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6
-* Copyright jQuery Foundation and other contributors; Licensed MIT */
-
-.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.003;filter:Alpha(Opacity=.3)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666}
\ No newline at end of file
diff --git a/vendor/jquery-ui/js/jquery-ui-1.12.1.min.js b/vendor/jquery-ui/js/jquery-ui-1.12.1.min.js
deleted file mode 100644
index 25398a1..0000000
--- a/vendor/jquery-ui/js/jquery-ui-1.12.1.min.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/*! jQuery UI - v1.12.1 - 2016-09-14
-* http://jqueryui.com
-* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js
-* Copyright jQuery Foundation and other contributors; Licensed MIT */
-
-(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function e(t){for(var e=t.css("visibility");"inherit"===e;)t=t.parent(),e=t.css("visibility");return"hidden"!==e}function i(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function s(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=n(t("
"))}function n(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,o)}function o(){t.datepicker._isDisabledDatepicker(m.inline?m.dpDiv.parent()[0]:m.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function a(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}function r(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.ui.version="1.12.1";var h=0,l=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],o=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=l.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=l.call(arguments,1),r=this;return o?this.length||"instance"!==n?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(r=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):r=void 0:(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),r}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"