@ngstarter-ui/components 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/fesm2022/ngstarter-ui-components-action-required.mjs +42 -0
- package/fesm2022/ngstarter-ui-components-action-required.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-alert.mjs +132 -0
- package/fesm2022/ngstarter-ui-components-alert.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-announcement.mjs +86 -0
- package/fesm2022/ngstarter-ui-components-announcement.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-autocomplete.mjs +360 -0
- package/fesm2022/ngstarter-ui-components-autocomplete.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-avatar.mjs +235 -0
- package/fesm2022/ngstarter-ui-components-avatar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-badge.mjs +97 -0
- package/fesm2022/ngstarter-ui-components-badge.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-block-loader.mjs +48 -0
- package/fesm2022/ngstarter-ui-components-block-loader.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-bottom-sheet.mjs +327 -0
- package/fesm2022/ngstarter-ui-components-bottom-sheet.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-breadcrumbs.mjs +209 -0
- package/fesm2022/ngstarter-ui-components-breadcrumbs.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-button-toggle.mjs +175 -0
- package/fesm2022/ngstarter-ui-components-button-toggle.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-button.mjs +70 -0
- package/fesm2022/ngstarter-ui-components-button.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-card-overlay.mjs +49 -0
- package/fesm2022/ngstarter-ui-components-card-overlay.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-card.mjs +199 -0
- package/fesm2022/ngstarter-ui-components-card.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-carousel.mjs +614 -0
- package/fesm2022/ngstarter-ui-components-carousel.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-checkbox.mjs +300 -0
- package/fesm2022/ngstarter-ui-components-checkbox.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-chips.mjs +589 -0
- package/fesm2022/ngstarter-ui-components-chips.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-code-highlighter.mjs +347 -0
- package/fesm2022/ngstarter-ui-components-code-highlighter.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-color-picker.mjs +713 -0
- package/fesm2022/ngstarter-ui-components-color-picker.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-color-scheme.mjs +106 -0
- package/fesm2022/ngstarter-ui-components-color-scheme.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-color-switcher.mjs +72 -0
- package/fesm2022/ngstarter-ui-components-color-switcher.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-command-bar.mjs +57 -0
- package/fesm2022/ngstarter-ui-components-command-bar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-comment-editor.mjs +1024 -0
- package/fesm2022/ngstarter-ui-components-comment-editor.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-comparison-slider.mjs +177 -0
- package/fesm2022/ngstarter-ui-components-comparison-slider.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-confirm.mjs +85 -0
- package/fesm2022/ngstarter-ui-components-confirm.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-code-block.component-Bk6QTli8.mjs +173 -0
- package/fesm2022/ngstarter-ui-components-content-editor-code-block.component-Bk6QTli8.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-content-editor-content-editable.directive-Bvfa2dqh.mjs +124 -0
- package/fesm2022/ngstarter-ui-components-content-editor-content-editor-content-editable.directive-Bvfa2dqh.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-cursor-controller-4Ak8VqGX.mjs +99 -0
- package/fesm2022/ngstarter-ui-components-content-editor-cursor-controller-4Ak8VqGX.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-divider-block.component-C_iRTCPH.mjs +33 -0
- package/fesm2022/ngstarter-ui-components-content-editor-divider-block.component-C_iRTCPH.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-embed-block-BbkC_t86.mjs +354 -0
- package/fesm2022/ngstarter-ui-components-content-editor-embed-block-BbkC_t86.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-heading-block.component-D9_CxTY1.mjs +114 -0
- package/fesm2022/ngstarter-ui-components-content-editor-heading-block.component-D9_CxTY1.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-image-block.component-B4zJyUg1.mjs +146 -0
- package/fesm2022/ngstarter-ui-components-content-editor-image-block.component-B4zJyUg1.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-list-block.component-Cv6wx5Xe.mjs +215 -0
- package/fesm2022/ngstarter-ui-components-content-editor-list-block.component-Cv6wx5Xe.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-ngstarter-ui-components-content-editor-1Zi2nAX5.mjs +2548 -0
- package/fesm2022/ngstarter-ui-components-content-editor-ngstarter-ui-components-content-editor-1Zi2nAX5.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-paragraph-block.component-C9bQvDYU.mjs +110 -0
- package/fesm2022/ngstarter-ui-components-content-editor-paragraph-block.component-C9bQvDYU.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-quote-block.component-BbHds2r2.mjs +141 -0
- package/fesm2022/ngstarter-ui-components-content-editor-quote-block.component-BbHds2r2.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-table-block.component-DlDh7Fnn.mjs +1604 -0
- package/fesm2022/ngstarter-ui-components-content-editor-table-block.component-DlDh7Fnn.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor-video-block.component-m4DTihP2.mjs +175 -0
- package/fesm2022/ngstarter-ui-components-content-editor-video-block.component-m4DTihP2.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-editor.mjs +2 -0
- package/fesm2022/ngstarter-ui-components-content-editor.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-content-fade.mjs +35 -0
- package/fesm2022/ngstarter-ui-components-content-fade.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-cookie-popup.mjs +107 -0
- package/fesm2022/ngstarter-ui-components-cookie-popup.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-core.mjs +1330 -0
- package/fesm2022/ngstarter-ui-components-core.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-country-select.mjs +489 -0
- package/fesm2022/ngstarter-ui-components-country-select.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-crop.mjs +183 -0
- package/fesm2022/ngstarter-ui-components-crop.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-currency-select.mjs +397 -0
- package/fesm2022/ngstarter-ui-components-currency-select.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-data-view.mjs +1494 -0
- package/fesm2022/ngstarter-ui-components-data-view.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-date-format-select.mjs +154 -0
- package/fesm2022/ngstarter-ui-components-date-format-select.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-datepicker.mjs +1159 -0
- package/fesm2022/ngstarter-ui-components-datepicker.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-dialog.mjs +357 -0
- package/fesm2022/ngstarter-ui-components-dialog.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-divider.mjs +42 -0
- package/fesm2022/ngstarter-ui-components-divider.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-drawer.mjs +132 -0
- package/fesm2022/ngstarter-ui-components-drawer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-emoji-picker.mjs +245 -0
- package/fesm2022/ngstarter-ui-components-emoji-picker.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-empty-state.mjs +75 -0
- package/fesm2022/ngstarter-ui-components-empty-state.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-expand.mjs +56 -0
- package/fesm2022/ngstarter-ui-components-expand.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-expansion.mjs +193 -0
- package/fesm2022/ngstarter-ui-components-expansion.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-filter-builder.mjs +333 -0
- package/fesm2022/ngstarter-ui-components-filter-builder.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-field.mjs +230 -0
- package/fesm2022/ngstarter-ui-components-form-field.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-autocomplete-many-field-BKQVlZHV.mjs +124 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-autocomplete-many-field-BKQVlZHV.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-checkbox-field-CoyKdvhV.mjs +22 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-checkbox-field-CoyKdvhV.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-datepicker-field-Bzc0TPO9.mjs +44 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-datepicker-field-Bzc0TPO9.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-divider-content-CwGzDCZv.mjs +17 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-divider-content-CwGzDCZv.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-image-content-ICTwkZPa.mjs +17 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-image-content-ICTwkZPa.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-input-field-RYxi-Mpw.mjs +35 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-input-field-RYxi-Mpw.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-radio-group-field-Cv3AGpoq.mjs +38 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-radio-group-field-Cv3AGpoq.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-select-field-eLcwI-BY.mjs +39 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-select-field-eLcwI-BY.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-text-content-BjzH_M3-.mjs +24 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-text-content-BjzH_M3-.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-textarea-field-4zH7FTQ1.mjs +37 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-textarea-field-4zH7FTQ1.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-timezone-field-BpH65Hd-.mjs +35 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-timezone-field-BpH65Hd-.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-toggle-field-iyqUrWxt.mjs +22 -0
- package/fesm2022/ngstarter-ui-components-form-renderer-toggle-field-iyqUrWxt.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-form-renderer.mjs +317 -0
- package/fesm2022/ngstarter-ui-components-form-renderer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-gauge.mjs +44 -0
- package/fesm2022/ngstarter-ui-components-gauge.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-grid.mjs +78 -0
- package/fesm2022/ngstarter-ui-components-grid.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-guided-tour.mjs +736 -0
- package/fesm2022/ngstarter-ui-components-guided-tour.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-headless-stepper.mjs +192 -0
- package/fesm2022/ngstarter-ui-components-headless-stepper.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-icon.mjs +61 -0
- package/fesm2022/ngstarter-ui-components-icon.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-image-designer.mjs +4016 -0
- package/fesm2022/ngstarter-ui-components-image-designer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-image-placeholder.mjs +20 -0
- package/fesm2022/ngstarter-ui-components-image-placeholder.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-image-resizer.mjs +151 -0
- package/fesm2022/ngstarter-ui-components-image-resizer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-image-viewer.mjs +349 -0
- package/fesm2022/ngstarter-ui-components-image-viewer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-image-zoom-viewer.mjs +162 -0
- package/fesm2022/ngstarter-ui-components-image-zoom-viewer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-incidents.mjs +257 -0
- package/fesm2022/ngstarter-ui-components-incidents.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-inline-text-edit.mjs +179 -0
- package/fesm2022/ngstarter-ui-components-inline-text-edit.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-input-mask.mjs +180 -0
- package/fesm2022/ngstarter-ui-components-input-mask.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-input-validator.mjs +24 -0
- package/fesm2022/ngstarter-ui-components-input-validator.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-input.mjs +152 -0
- package/fesm2022/ngstarter-ui-components-input.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-kanban-board.mjs +156 -0
- package/fesm2022/ngstarter-ui-components-kanban-board.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-kbd.mjs +31 -0
- package/fesm2022/ngstarter-ui-components-kbd.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-layout.mjs +199 -0
- package/fesm2022/ngstarter-ui-components-layout.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-list.mjs +279 -0
- package/fesm2022/ngstarter-ui-components-list.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-logo.mjs +51 -0
- package/fesm2022/ngstarter-ui-components-logo.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-marquee.mjs +76 -0
- package/fesm2022/ngstarter-ui-components-marquee.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-menu.mjs +851 -0
- package/fesm2022/ngstarter-ui-components-menu.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-micro-chart.mjs +928 -0
- package/fesm2022/ngstarter-ui-components-micro-chart.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-navigation.mjs +439 -0
- package/fesm2022/ngstarter-ui-components-navigation.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-notifications.mjs +181 -0
- package/fesm2022/ngstarter-ui-components-notifications.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-number-input.mjs +293 -0
- package/fesm2022/ngstarter-ui-components-number-input.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-option.mjs +157 -0
- package/fesm2022/ngstarter-ui-components-option.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-overlay.mjs +112 -0
- package/fesm2022/ngstarter-ui-components-overlay.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-page-loading-bar.mjs +77 -0
- package/fesm2022/ngstarter-ui-components-page-loading-bar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-paginator.mjs +297 -0
- package/fesm2022/ngstarter-ui-components-paginator.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-panel.mjs +123 -0
- package/fesm2022/ngstarter-ui-components-panel.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-password-strength.mjs +335 -0
- package/fesm2022/ngstarter-ui-components-password-strength.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-phone-input.mjs +651 -0
- package/fesm2022/ngstarter-ui-components-phone-input.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-pin-input.mjs +193 -0
- package/fesm2022/ngstarter-ui-components-pin-input.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-popover.mjs +302 -0
- package/fesm2022/ngstarter-ui-components-popover.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-progress-bar.mjs +68 -0
- package/fesm2022/ngstarter-ui-components-progress-bar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-radio-card.mjs +102 -0
- package/fesm2022/ngstarter-ui-components-radio-card.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-radio.mjs +147 -0
- package/fesm2022/ngstarter-ui-components-radio.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-rail-nav.mjs +87 -0
- package/fesm2022/ngstarter-ui-components-rail-nav.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-resizable-container.mjs +74 -0
- package/fesm2022/ngstarter-ui-components-resizable-container.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-screen-loader.mjs +95 -0
- package/fesm2022/ngstarter-ui-components-screen-loader.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-scroll-spy.mjs +219 -0
- package/fesm2022/ngstarter-ui-components-scroll-spy.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-scrollbar-area.mjs +459 -0
- package/fesm2022/ngstarter-ui-components-scrollbar-area.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-segmented.mjs +218 -0
- package/fesm2022/ngstarter-ui-components-segmented.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-select.mjs +496 -0
- package/fesm2022/ngstarter-ui-components-select.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-side-panel.mjs +107 -0
- package/fesm2022/ngstarter-ui-components-side-panel.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-sidebar.mjs +435 -0
- package/fesm2022/ngstarter-ui-components-sidebar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-sidenav.mjs +354 -0
- package/fesm2022/ngstarter-ui-components-sidenav.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-signature-pad.mjs +452 -0
- package/fesm2022/ngstarter-ui-components-signature-pad.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-skeleton.mjs +22 -0
- package/fesm2022/ngstarter-ui-components-skeleton.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-slide-toggle.mjs +93 -0
- package/fesm2022/ngstarter-ui-components-slide-toggle.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-slider.mjs +481 -0
- package/fesm2022/ngstarter-ui-components-slider.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-snack-bar.mjs +354 -0
- package/fesm2022/ngstarter-ui-components-snack-bar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-sort.mjs +140 -0
- package/fesm2022/ngstarter-ui-components-sort.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-spinner.mjs +75 -0
- package/fesm2022/ngstarter-ui-components-spinner.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-splash-screen.mjs +93 -0
- package/fesm2022/ngstarter-ui-components-splash-screen.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-split.mjs +948 -0
- package/fesm2022/ngstarter-ui-components-split.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-stepper.mjs +103 -0
- package/fesm2022/ngstarter-ui-components-stepper.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-suggestions.mjs +72 -0
- package/fesm2022/ngstarter-ui-components-suggestions.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-tab-panel.mjs +265 -0
- package/fesm2022/ngstarter-ui-components-tab-panel.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-table.mjs +648 -0
- package/fesm2022/ngstarter-ui-components-table.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-tabs.mjs +591 -0
- package/fesm2022/ngstarter-ui-components-tabs.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-text-editor.mjs +1012 -0
- package/fesm2022/ngstarter-ui-components-text-editor.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-thumbnail-maker.mjs +212 -0
- package/fesm2022/ngstarter-ui-components-thumbnail-maker.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-tiles.mjs +634 -0
- package/fesm2022/ngstarter-ui-components-tiles.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-timeline.mjs +122 -0
- package/fesm2022/ngstarter-ui-components-timeline.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-timepicker.mjs +486 -0
- package/fesm2022/ngstarter-ui-components-timepicker.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-timezone-select.mjs +371 -0
- package/fesm2022/ngstarter-ui-components-timezone-select.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-toolbar.mjs +299 -0
- package/fesm2022/ngstarter-ui-components-toolbar.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-tooltip.mjs +506 -0
- package/fesm2022/ngstarter-ui-components-tooltip.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-tree.mjs +200 -0
- package/fesm2022/ngstarter-ui-components-tree.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-upload.mjs +330 -0
- package/fesm2022/ngstarter-ui-components-upload.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-video-player.mjs +516 -0
- package/fesm2022/ngstarter-ui-components-video-player.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-video-viewer.mjs +218 -0
- package/fesm2022/ngstarter-ui-components-video-viewer.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-visual-builder.mjs +18 -0
- package/fesm2022/ngstarter-ui-components-visual-builder.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components.mjs +6 -0
- package/fesm2022/ngstarter-ui-components.mjs.map +1 -0
- package/package.json +535 -0
- package/styles/_common.scss +456 -0
- package/styles/_global.scss +91 -0
- package/styles/themes/default.scss +2 -0
- package/types/ngstarter-ui-components-action-required.d.ts +14 -0
- package/types/ngstarter-ui-components-alert.d.ts +50 -0
- package/types/ngstarter-ui-components-announcement.d.ts +59 -0
- package/types/ngstarter-ui-components-autocomplete.d.ts +83 -0
- package/types/ngstarter-ui-components-avatar.d.ts +69 -0
- package/types/ngstarter-ui-components-badge.d.ts +38 -0
- package/types/ngstarter-ui-components-block-loader.d.ts +21 -0
- package/types/ngstarter-ui-components-bottom-sheet.d.ts +149 -0
- package/types/ngstarter-ui-components-breadcrumbs.d.ts +104 -0
- package/types/ngstarter-ui-components-button-toggle.d.ts +54 -0
- package/types/ngstarter-ui-components-button.d.ts +27 -0
- package/types/ngstarter-ui-components-card-overlay.d.ts +20 -0
- package/types/ngstarter-ui-components-card.d.ts +85 -0
- package/types/ngstarter-ui-components-carousel.d.ts +76 -0
- package/types/ngstarter-ui-components-checkbox.d.ts +94 -0
- package/types/ngstarter-ui-components-chips.d.ts +189 -0
- package/types/ngstarter-ui-components-code-highlighter.d.ts +28 -0
- package/types/ngstarter-ui-components-color-picker.d.ts +92 -0
- package/types/ngstarter-ui-components-color-scheme.d.ts +44 -0
- package/types/ngstarter-ui-components-color-switcher.d.ts +26 -0
- package/types/ngstarter-ui-components-command-bar.d.ts +28 -0
- package/types/ngstarter-ui-components-comment-editor.d.ts +194 -0
- package/types/ngstarter-ui-components-comparison-slider.d.ts +42 -0
- package/types/ngstarter-ui-components-confirm.d.ts +34 -0
- package/types/ngstarter-ui-components-content-editor.d.ts +321 -0
- package/types/ngstarter-ui-components-content-fade.d.ts +17 -0
- package/types/ngstarter-ui-components-cookie-popup.d.ts +41 -0
- package/types/ngstarter-ui-components-core.d.ts +421 -0
- package/types/ngstarter-ui-components-country-select.d.ts +78 -0
- package/types/ngstarter-ui-components-crop.d.ts +59 -0
- package/types/ngstarter-ui-components-currency-select.d.ts +82 -0
- package/types/ngstarter-ui-components-data-view.d.ts +391 -0
- package/types/ngstarter-ui-components-date-format-select.d.ts +59 -0
- package/types/ngstarter-ui-components-datepicker.d.ts +384 -0
- package/types/ngstarter-ui-components-dialog.d.ts +115 -0
- package/types/ngstarter-ui-components-divider.d.ts +18 -0
- package/types/ngstarter-ui-components-drawer.d.ts +32 -0
- package/types/ngstarter-ui-components-emoji-picker.d.ts +49 -0
- package/types/ngstarter-ui-components-empty-state.d.ts +33 -0
- package/types/ngstarter-ui-components-expand.d.ts +26 -0
- package/types/ngstarter-ui-components-expansion.d.ts +68 -0
- package/types/ngstarter-ui-components-filter-builder.d.ts +106 -0
- package/types/ngstarter-ui-components-form-field.d.ts +107 -0
- package/types/ngstarter-ui-components-form-renderer.d.ts +121 -0
- package/types/ngstarter-ui-components-gauge.d.ts +21 -0
- package/types/ngstarter-ui-components-grid.d.ts +45 -0
- package/types/ngstarter-ui-components-guided-tour.d.ts +227 -0
- package/types/ngstarter-ui-components-headless-stepper.d.ts +65 -0
- package/types/ngstarter-ui-components-icon.d.ts +17 -0
- package/types/ngstarter-ui-components-image-designer.d.ts +357 -0
- package/types/ngstarter-ui-components-image-placeholder.d.ts +8 -0
- package/types/ngstarter-ui-components-image-resizer.d.ts +35 -0
- package/types/ngstarter-ui-components-image-viewer.d.ts +63 -0
- package/types/ngstarter-ui-components-image-zoom-viewer.d.ts +34 -0
- package/types/ngstarter-ui-components-incidents.d.ts +119 -0
- package/types/ngstarter-ui-components-inline-text-edit.d.ts +39 -0
- package/types/ngstarter-ui-components-input-mask.d.ts +36 -0
- package/types/ngstarter-ui-components-input-validator.d.ts +5 -0
- package/types/ngstarter-ui-components-input.d.ts +53 -0
- package/types/ngstarter-ui-components-kanban-board.d.ts +68 -0
- package/types/ngstarter-ui-components-kbd.d.ts +13 -0
- package/types/ngstarter-ui-components-layout.d.ts +83 -0
- package/types/ngstarter-ui-components-list.d.ts +98 -0
- package/types/ngstarter-ui-components-logo.d.ts +26 -0
- package/types/ngstarter-ui-components-marquee.d.ts +27 -0
- package/types/ngstarter-ui-components-menu.d.ts +199 -0
- package/types/ngstarter-ui-components-micro-chart.d.ts +195 -0
- package/types/ngstarter-ui-components-navigation.d.ts +136 -0
- package/types/ngstarter-ui-components-notifications.d.ts +84 -0
- package/types/ngstarter-ui-components-number-input.d.ts +99 -0
- package/types/ngstarter-ui-components-option.d.ts +61 -0
- package/types/ngstarter-ui-components-overlay.d.ts +12 -0
- package/types/ngstarter-ui-components-page-loading-bar.d.ts +20 -0
- package/types/ngstarter-ui-components-paginator.d.ts +145 -0
- package/types/ngstarter-ui-components-panel.d.ts +59 -0
- package/types/ngstarter-ui-components-password-strength.d.ts +109 -0
- package/types/ngstarter-ui-components-phone-input.d.ts +103 -0
- package/types/ngstarter-ui-components-pin-input.d.ts +48 -0
- package/types/ngstarter-ui-components-popover.d.ts +94 -0
- package/types/ngstarter-ui-components-progress-bar.d.ts +30 -0
- package/types/ngstarter-ui-components-radio-card.d.ts +37 -0
- package/types/ngstarter-ui-components-radio.d.ts +45 -0
- package/types/ngstarter-ui-components-rail-nav.d.ts +36 -0
- package/types/ngstarter-ui-components-resizable-container.d.ts +25 -0
- package/types/ngstarter-ui-components-screen-loader.d.ts +34 -0
- package/types/ngstarter-ui-components-scroll-spy.d.ts +63 -0
- package/types/ngstarter-ui-components-scrollbar-area.d.ts +67 -0
- package/types/ngstarter-ui-components-segmented.d.ts +65 -0
- package/types/ngstarter-ui-components-select.d.ts +126 -0
- package/types/ngstarter-ui-components-side-panel.d.ts +42 -0
- package/types/ngstarter-ui-components-sidebar.d.ts +143 -0
- package/types/ngstarter-ui-components-sidenav.d.ts +86 -0
- package/types/ngstarter-ui-components-signature-pad.d.ts +49 -0
- package/types/ngstarter-ui-components-skeleton.d.ts +9 -0
- package/types/ngstarter-ui-components-slide-toggle.d.ts +41 -0
- package/types/ngstarter-ui-components-slider.d.ts +85 -0
- package/types/ngstarter-ui-components-snack-bar.d.ts +142 -0
- package/types/ngstarter-ui-components-sort.d.ts +66 -0
- package/types/ngstarter-ui-components-spinner.d.ts +28 -0
- package/types/ngstarter-ui-components-splash-screen.d.ts +31 -0
- package/types/ngstarter-ui-components-split.d.ts +210 -0
- package/types/ngstarter-ui-components-stepper.d.ts +44 -0
- package/types/ngstarter-ui-components-suggestions.d.ts +32 -0
- package/types/ngstarter-ui-components-tab-panel.d.ts +96 -0
- package/types/ngstarter-ui-components-table.d.ts +277 -0
- package/types/ngstarter-ui-components-tabs.d.ts +145 -0
- package/types/ngstarter-ui-components-text-editor.d.ts +191 -0
- package/types/ngstarter-ui-components-thumbnail-maker.d.ts +35 -0
- package/types/ngstarter-ui-components-tiles.d.ts +109 -0
- package/types/ngstarter-ui-components-timeline.d.ts +57 -0
- package/types/ngstarter-ui-components-timepicker.d.ts +115 -0
- package/types/ngstarter-ui-components-timezone-select.d.ts +75 -0
- package/types/ngstarter-ui-components-toolbar.d.ts +74 -0
- package/types/ngstarter-ui-components-tooltip.d.ts +52 -0
- package/types/ngstarter-ui-components-tree.d.ts +60 -0
- package/types/ngstarter-ui-components-upload.d.ts +134 -0
- package/types/ngstarter-ui-components-video-player.d.ts +67 -0
- package/types/ngstarter-ui-components-video-viewer.d.ts +98 -0
- package/types/ngstarter-ui-components-visual-builder.d.ts +8 -0
- package/types/ngstarter-ui-components.d.ts +2 -0
|
@@ -0,0 +1,4016 @@
|
|
|
1
|
+
import { moveItemInArray, CdkDropList, CdkDrag, CdkDragPlaceholder } from '@angular/cdk/drag-drop';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { signal, inject, PLATFORM_ID, Injectable, InjectionToken, computed, ChangeDetectionStrategy, Component, DestroyRef, viewChild, input, booleanAttribute, output, numberAttribute, effect, untracked, forwardRef } from '@angular/core';
|
|
4
|
+
import * as i1 from '@angular/common';
|
|
5
|
+
import { isPlatformBrowser, CommonModule, DecimalPipe } from '@angular/common';
|
|
6
|
+
import { HttpClient } from '@angular/common/http';
|
|
7
|
+
import { Subject, map, isObservable } from 'rxjs';
|
|
8
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
|
+
import { Panel, PanelHeader, PanelContent, PanelSidebar } from '@ngstarter-ui/components/panel';
|
|
10
|
+
import { Toolbar, ToolbarItem, ToolbarSpacer, ToolbarTitle } from '@ngstarter-ui/components/toolbar';
|
|
11
|
+
import { Button } from '@ngstarter-ui/components/button';
|
|
12
|
+
import { Divider } from '@ngstarter-ui/components/divider';
|
|
13
|
+
import { Icon } from '@ngstarter-ui/components/icon';
|
|
14
|
+
import { TabPanel, TabPanelAside, TabPanelAsideContentDirective, TabPanelContent, TabPanelItem, TabPanelItemIconDirective, TabPanelItemText, TabPanelNav } from '@ngstarter-ui/components/tab-panel';
|
|
15
|
+
import { UploadTriggerDirective } from '@ngstarter-ui/components/upload';
|
|
16
|
+
import { List, ListItem, ListItemLine } from '@ngstarter-ui/components/list';
|
|
17
|
+
import { ProgressSpinner } from '@ngstarter-ui/components/spinner';
|
|
18
|
+
import Konva from 'konva';
|
|
19
|
+
import * as i1$1 from '@angular/forms';
|
|
20
|
+
import { FormsModule } from '@angular/forms';
|
|
21
|
+
import { TabGroup, Tab } from '@ngstarter-ui/components/tabs';
|
|
22
|
+
import { ColorSwitcher } from '@ngstarter-ui/components/color-switcher';
|
|
23
|
+
import { Accordion, ExpansionPanel, ExpansionPanelHeader, ExpansionPanelTitle } from '@ngstarter-ui/components/expansion';
|
|
24
|
+
import { FormField, Label } from '@ngstarter-ui/components/form-field';
|
|
25
|
+
import { Input } from '@ngstarter-ui/components/input';
|
|
26
|
+
import { Select, Option } from '@ngstarter-ui/components/select';
|
|
27
|
+
import { ScrollbarArea } from '@ngstarter-ui/components/scrollbar-area';
|
|
28
|
+
import { ComponentPortal, CdkPortalOutlet } from '@angular/cdk/portal';
|
|
29
|
+
import { Slider, SliderThumb } from '@ngstarter-ui/components/slider';
|
|
30
|
+
import { Popover, PopoverContent, PopoverTriggerForDirective } from '@ngstarter-ui/components/popover';
|
|
31
|
+
import { Menu, MenuItem, MenuTrigger } from '@ngstarter-ui/components/menu';
|
|
32
|
+
import { ButtonToggle, ButtonToggleGroup } from '@ngstarter-ui/components/button-toggle';
|
|
33
|
+
import { Ripple } from '@ngstarter-ui/components/core';
|
|
34
|
+
import { SlideToggle } from '@ngstarter-ui/components/slide-toggle';
|
|
35
|
+
import { ColorPicker, ColorPickerThumbnail, ColorPickerTriggerForDirective } from '@ngstarter-ui/components/color-picker';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A custom Konva filter for color tinting.
|
|
39
|
+
* It modifies existing pixel colors by a factor without completely replacing them.
|
|
40
|
+
* Factors are between -1 and 1.
|
|
41
|
+
*/
|
|
42
|
+
const TintFilter = function (imageData) {
|
|
43
|
+
const data = imageData.data;
|
|
44
|
+
const n = data.length;
|
|
45
|
+
const tintR = this.getAttr('tintR') || 0;
|
|
46
|
+
const tintG = this.getAttr('tintG') || 0;
|
|
47
|
+
const tintB = this.getAttr('tintB') || 0;
|
|
48
|
+
if (tintR === 0 && tintG === 0 && tintB === 0)
|
|
49
|
+
return;
|
|
50
|
+
for (let i = 0; i < n; i += 4) {
|
|
51
|
+
if (tintR > 0)
|
|
52
|
+
data[i] += (255 - data[i]) * tintR;
|
|
53
|
+
else if (tintR < 0)
|
|
54
|
+
data[i] += data[i] * tintR;
|
|
55
|
+
if (tintG > 0)
|
|
56
|
+
data[i + 1] += (255 - data[i + 1]) * tintG;
|
|
57
|
+
else if (tintG < 0)
|
|
58
|
+
data[i + 1] += data[i + 1] * tintG;
|
|
59
|
+
if (tintB > 0)
|
|
60
|
+
data[i + 2] += (255 - data[i + 2]) * tintB;
|
|
61
|
+
else if (tintB < 0)
|
|
62
|
+
data[i + 2] += data[i + 2] * tintB;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
class ImageDesignerService {
|
|
66
|
+
stage;
|
|
67
|
+
workspaceLayer;
|
|
68
|
+
uiLayer;
|
|
69
|
+
canvasRect;
|
|
70
|
+
maskOverlay;
|
|
71
|
+
resizeObserver;
|
|
72
|
+
minScale = 0.1;
|
|
73
|
+
maxScale = 5;
|
|
74
|
+
scale = signal(1, ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
|
|
75
|
+
isInitialized = signal(false, ...(ngDevMode ? [{ debugName: "isInitialized" }] : /* istanbul ignore next */ []));
|
|
76
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
77
|
+
loadedFonts = new Set();
|
|
78
|
+
fonts = signal([
|
|
79
|
+
'Inter',
|
|
80
|
+
'Roboto',
|
|
81
|
+
'Open Sans',
|
|
82
|
+
'Lato',
|
|
83
|
+
'Montserrat',
|
|
84
|
+
'Oswald',
|
|
85
|
+
'Source Sans Pro',
|
|
86
|
+
'Slabo 27px',
|
|
87
|
+
'Raleway',
|
|
88
|
+
'PT Sans',
|
|
89
|
+
'Merriweather',
|
|
90
|
+
'Nunito',
|
|
91
|
+
'Playfair Display',
|
|
92
|
+
'Poppins',
|
|
93
|
+
'Ubuntu',
|
|
94
|
+
'Lora',
|
|
95
|
+
'Muli',
|
|
96
|
+
'Arvo',
|
|
97
|
+
'Rubik',
|
|
98
|
+
'Courier Prime',
|
|
99
|
+
'Pacifico',
|
|
100
|
+
'Dancing Script',
|
|
101
|
+
'Bangers',
|
|
102
|
+
'Special Elite',
|
|
103
|
+
'Press Start 2P',
|
|
104
|
+
'Monoton',
|
|
105
|
+
'Creepster',
|
|
106
|
+
'Lobster',
|
|
107
|
+
'Orbitron',
|
|
108
|
+
'Righteous',
|
|
109
|
+
'Satisfy',
|
|
110
|
+
'Shadows Into Light',
|
|
111
|
+
'Permanent Marker',
|
|
112
|
+
'Fredoka One',
|
|
113
|
+
'Amatic SC',
|
|
114
|
+
'Cinzel',
|
|
115
|
+
'Great Vibes',
|
|
116
|
+
'Kaushan Script'
|
|
117
|
+
], ...(ngDevMode ? [{ debugName: "fonts" }] : /* istanbul ignore next */ []));
|
|
118
|
+
_layers = signal([], ...(ngDevMode ? [{ debugName: "_layers" }] : /* istanbul ignore next */ []));
|
|
119
|
+
layers = this._layers.asReadonly();
|
|
120
|
+
selectedLayerId = signal(null, ...(ngDevMode ? [{ debugName: "selectedLayerId" }] : /* istanbul ignore next */ []));
|
|
121
|
+
_change$ = new Subject();
|
|
122
|
+
change$ = this._change$.asObservable();
|
|
123
|
+
undoStack = [];
|
|
124
|
+
redoStack = [];
|
|
125
|
+
canUndo = signal(false, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
|
|
126
|
+
canRedo = signal(false, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
|
|
127
|
+
historyLimit = 50;
|
|
128
|
+
transformer;
|
|
129
|
+
selectionRect;
|
|
130
|
+
hoverRect;
|
|
131
|
+
hoveredShape;
|
|
132
|
+
selectionBordersGroup;
|
|
133
|
+
selectionStartPos;
|
|
134
|
+
isSelecting = false;
|
|
135
|
+
isDragging = false;
|
|
136
|
+
wasSelectedBeforeClick = false;
|
|
137
|
+
snapLines = [];
|
|
138
|
+
snapSettings = {
|
|
139
|
+
showGuidelines: true,
|
|
140
|
+
snapToShapes: true,
|
|
141
|
+
snapToStageCenter: true,
|
|
142
|
+
snapToStageBorders: true,
|
|
143
|
+
guidelineColor: 'blue',
|
|
144
|
+
snapRange: 5
|
|
145
|
+
};
|
|
146
|
+
defaultFont = 'Inter';
|
|
147
|
+
ngOnDestroy() {
|
|
148
|
+
this.destroy();
|
|
149
|
+
}
|
|
150
|
+
async init(container, imageSize, defaultFont = 'Inter', scale = 1, minScale = 0.1, maxScale = 5) {
|
|
151
|
+
if (!this.isBrowser) {
|
|
152
|
+
console.log('ImageDesignerService.init skipped (not a browser)');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log('ImageDesignerService.init starting', { container, imageSize, defaultFont, scale, minScale, maxScale });
|
|
156
|
+
console.log('Container dimensions:', container.offsetWidth, 'x', container.offsetHeight);
|
|
157
|
+
this.defaultFont = defaultFont;
|
|
158
|
+
this.minScale = minScale;
|
|
159
|
+
this.maxScale = maxScale;
|
|
160
|
+
this.setScale(scale);
|
|
161
|
+
try {
|
|
162
|
+
this.stage = new Konva.Stage({
|
|
163
|
+
container: container,
|
|
164
|
+
width: container.offsetWidth || imageSize.width + 100,
|
|
165
|
+
height: container.offsetHeight || imageSize.height + 100,
|
|
166
|
+
});
|
|
167
|
+
console.log('Konva.Stage created');
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
console.error('Failed to create Konva.Stage:', e);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.workspaceLayer = new Konva.Layer();
|
|
174
|
+
this.stage.add(this.workspaceLayer);
|
|
175
|
+
this.uiLayer = new Konva.Layer();
|
|
176
|
+
this.stage.add(this.uiLayer);
|
|
177
|
+
this.canvasRect = new Konva.Rect({
|
|
178
|
+
x: (this.stage.width() || imageSize.width) / 2,
|
|
179
|
+
y: (this.stage.height() || imageSize.height) / 2,
|
|
180
|
+
width: imageSize.width,
|
|
181
|
+
height: imageSize.height,
|
|
182
|
+
fill: 'white',
|
|
183
|
+
stroke: '#d9dcdf',
|
|
184
|
+
strokeWidth: 1,
|
|
185
|
+
name: 'canvasRect'
|
|
186
|
+
});
|
|
187
|
+
this.canvasRect.offsetX(imageSize.width / 2);
|
|
188
|
+
this.canvasRect.offsetY(imageSize.height / 2);
|
|
189
|
+
this.canvasRect.scaleX(this.scale());
|
|
190
|
+
this.canvasRect.scaleY(this.scale());
|
|
191
|
+
this.workspaceLayer.add(this.canvasRect);
|
|
192
|
+
this.transformer = new Konva.Transformer({
|
|
193
|
+
rotateEnabled: true,
|
|
194
|
+
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
|
|
195
|
+
name: 'transformer',
|
|
196
|
+
// Ensure transformer is always above the mask
|
|
197
|
+
boundBoxFunc: (oldBox, newBox) => {
|
|
198
|
+
// Limit minimum size
|
|
199
|
+
if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
|
|
200
|
+
return oldBox;
|
|
201
|
+
}
|
|
202
|
+
return newBox;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
this.uiLayer.add(this.transformer);
|
|
206
|
+
this.transformer.on('transform', () => {
|
|
207
|
+
this.updateSelectionBorders();
|
|
208
|
+
const nodes = this.transformer?.nodes() || [];
|
|
209
|
+
nodes.forEach((node) => {
|
|
210
|
+
if (node instanceof Konva.Text) {
|
|
211
|
+
// If we resized vertically (scaleY changed), scale fontSize
|
|
212
|
+
const scaleY = node.scaleY();
|
|
213
|
+
if (Math.abs(scaleY - this.scale()) > 0.001) {
|
|
214
|
+
node.fontSize(node.fontSize() * (scaleY / this.scale()));
|
|
215
|
+
}
|
|
216
|
+
// Scale width according to scaleX
|
|
217
|
+
const scaleX = node.scaleX();
|
|
218
|
+
node.width(node.width() * (scaleX / this.scale()));
|
|
219
|
+
// Reset scale to current canvas scale
|
|
220
|
+
node.scaleX(this.scale());
|
|
221
|
+
node.scaleY(this.scale());
|
|
222
|
+
// Update offset because width/fontSize changed
|
|
223
|
+
node.offsetX(node.width() / 2);
|
|
224
|
+
node.offsetY(node.height() / 2);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (nodes.length > 0) {
|
|
228
|
+
const guides = this.getSnappingGuides(nodes);
|
|
229
|
+
this.drawSnappingLines(guides);
|
|
230
|
+
this.updateShapesOriginalPositions(nodes);
|
|
231
|
+
}
|
|
232
|
+
this.notifyChange();
|
|
233
|
+
});
|
|
234
|
+
this.transformer.on('dragmove', (e) => {
|
|
235
|
+
const target = e.target;
|
|
236
|
+
if (this.transformer && target === this.transformer) {
|
|
237
|
+
const nodes = this.transformer.nodes();
|
|
238
|
+
if (nodes.length > 0) {
|
|
239
|
+
const targetBox = this.getNodesClientRect(nodes);
|
|
240
|
+
const guides = this.getSnappingGuides(nodes);
|
|
241
|
+
let dx = 0;
|
|
242
|
+
let dy = 0;
|
|
243
|
+
const vGuide = guides.vertical[0];
|
|
244
|
+
const hGuide = guides.horizontal[0];
|
|
245
|
+
if (vGuide) {
|
|
246
|
+
dx = vGuide.guide - vGuide.offset - targetBox.x;
|
|
247
|
+
}
|
|
248
|
+
if (hGuide) {
|
|
249
|
+
dy = hGuide.guide - hGuide.offset - targetBox.y;
|
|
250
|
+
}
|
|
251
|
+
if (dx !== 0 || dy !== 0) {
|
|
252
|
+
nodes.forEach((node) => {
|
|
253
|
+
const p = node.getAbsolutePosition();
|
|
254
|
+
node.setAbsolutePosition({
|
|
255
|
+
x: p.x + dx,
|
|
256
|
+
y: p.y + dy
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const finalGuides = this.getSnappingGuides(nodes);
|
|
261
|
+
this.drawSnappingLines(finalGuides);
|
|
262
|
+
this.updateSelectionBorders();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
this.transformer.on('dragend', () => {
|
|
267
|
+
this.clearSnapLines();
|
|
268
|
+
const nodes = this.transformer?.nodes() || [];
|
|
269
|
+
if (nodes.length > 0) {
|
|
270
|
+
this.updateShapesOriginalPositions(nodes);
|
|
271
|
+
}
|
|
272
|
+
this.notifyChange();
|
|
273
|
+
});
|
|
274
|
+
this.transformer.on('transformend', () => {
|
|
275
|
+
this.clearSnapLines();
|
|
276
|
+
const nodes = this.transformer?.nodes() || [];
|
|
277
|
+
if (nodes.length > 0) {
|
|
278
|
+
nodes.forEach(node => {
|
|
279
|
+
if (!(node instanceof Konva.Text)) {
|
|
280
|
+
const scaleX = node.scaleX();
|
|
281
|
+
const scaleY = node.scaleY();
|
|
282
|
+
node.width(node.width() * (scaleX / this.scale()));
|
|
283
|
+
node.height(node.height() * (scaleY / this.scale()));
|
|
284
|
+
node.scaleX(this.scale());
|
|
285
|
+
node.scaleY(this.scale());
|
|
286
|
+
}
|
|
287
|
+
// If the node has effects, we MUST re-cache it with the new size
|
|
288
|
+
const id = node.id();
|
|
289
|
+
const config = this._layers().find(l => l.id === id);
|
|
290
|
+
if (config) {
|
|
291
|
+
// Re-apply effects and re-cache
|
|
292
|
+
this.applyEffectsToShape(node, config);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
// We update the signal AFTER applying effects to ensure consistency
|
|
296
|
+
this.updateShapesOriginalPositions(nodes);
|
|
297
|
+
}
|
|
298
|
+
this.notifyChange();
|
|
299
|
+
});
|
|
300
|
+
this.selectionRect = new Konva.Rect({
|
|
301
|
+
fill: 'rgba(0,0,255,0.1)',
|
|
302
|
+
stroke: '#3b82f6',
|
|
303
|
+
strokeWidth: 1,
|
|
304
|
+
visible: false,
|
|
305
|
+
name: 'selectionRect',
|
|
306
|
+
zIndex: 1001
|
|
307
|
+
});
|
|
308
|
+
this.uiLayer.add(this.selectionRect);
|
|
309
|
+
this.hoverRect = new Konva.Rect({
|
|
310
|
+
stroke: '#3b82f6',
|
|
311
|
+
strokeWidth: 1,
|
|
312
|
+
listening: false,
|
|
313
|
+
visible: false,
|
|
314
|
+
name: 'hoverRect'
|
|
315
|
+
});
|
|
316
|
+
this.uiLayer.add(this.hoverRect);
|
|
317
|
+
this.selectionBordersGroup = new Konva.Group({
|
|
318
|
+
listening: false,
|
|
319
|
+
name: 'selectionBordersGroup'
|
|
320
|
+
});
|
|
321
|
+
this.uiLayer.add(this.selectionBordersGroup);
|
|
322
|
+
this.maskOverlay = new Konva.Shape({
|
|
323
|
+
fill: 'rgba(255, 255, 255, 0.8)',
|
|
324
|
+
listening: false,
|
|
325
|
+
name: 'maskOverlay',
|
|
326
|
+
zIndex: 1000,
|
|
327
|
+
fillRule: 'evenodd',
|
|
328
|
+
sceneFunc: (context, shape) => {
|
|
329
|
+
const stage = shape.getStage();
|
|
330
|
+
if (!stage || !this.canvasRect)
|
|
331
|
+
return;
|
|
332
|
+
const stageWidth = stage.width();
|
|
333
|
+
const stageHeight = stage.height();
|
|
334
|
+
const canvasX = this.canvasRect.x();
|
|
335
|
+
const canvasY = this.canvasRect.y();
|
|
336
|
+
const canvasWidth = this.canvasRect.width() * this.canvasRect.scaleX();
|
|
337
|
+
const canvasHeight = this.canvasRect.height() * this.canvasRect.scaleY();
|
|
338
|
+
context.beginPath();
|
|
339
|
+
// Наружный прямоугольник (весь Stage)
|
|
340
|
+
context.rect(0, 0, stageWidth, stageHeight);
|
|
341
|
+
// Внутренний прямоугольник (canvasRect)
|
|
342
|
+
// Since canvasRect has offset at its center, its top-left corner is at (x - width*scale/2, y - height*scale/2)
|
|
343
|
+
// We draw it in counter-clockwise direction to create a hole
|
|
344
|
+
context.rect(canvasX + canvasWidth / 2, canvasY - canvasHeight / 2, -canvasWidth, canvasHeight);
|
|
345
|
+
context.closePath();
|
|
346
|
+
context.fillShape(shape);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
this.uiLayer.add(this.maskOverlay);
|
|
350
|
+
await this.loadAllFonts();
|
|
351
|
+
this.updateSize(container, imageSize);
|
|
352
|
+
// Add a small delay to ensure everything is settled and then center again
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
this.updateSize(container, imageSize);
|
|
355
|
+
this.workspaceLayer?.batchDraw();
|
|
356
|
+
this.uiLayer?.batchDraw();
|
|
357
|
+
}, 50);
|
|
358
|
+
// Final draw to ensure everything is rendered
|
|
359
|
+
this.workspaceLayer?.batchDraw();
|
|
360
|
+
this.uiLayer?.batchDraw();
|
|
361
|
+
console.log('Konva Canvas Rect and layers added and drawn');
|
|
362
|
+
this.setupResizeObserver(container, imageSize);
|
|
363
|
+
this.setupSelectionListeners();
|
|
364
|
+
this.isInitialized.set(true);
|
|
365
|
+
this.undoStack = [this.getSnapshot()];
|
|
366
|
+
this.updateHistorySignals();
|
|
367
|
+
}
|
|
368
|
+
handleGlobalMouseUp = (e) => {
|
|
369
|
+
if (!this.isSelecting) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
this.isSelecting = false;
|
|
373
|
+
if (!this.selectionRect || !this.workspaceLayer || !this.transformer)
|
|
374
|
+
return;
|
|
375
|
+
if (!this.selectionRect.visible()) {
|
|
376
|
+
// If no selection rect was drawn, but we were selecting,
|
|
377
|
+
// it means it was a simple click that didn't move much.
|
|
378
|
+
const pos = this.stage?.getPointerPosition();
|
|
379
|
+
const shape = pos ? this.stage?.getIntersection(pos) : null;
|
|
380
|
+
if (shape && shape.id()) {
|
|
381
|
+
this.selectLayer(shape.id());
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
this.selectLayer(null);
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.selectionRect.visible(false);
|
|
389
|
+
const box = this.selectionRect.getClientRect();
|
|
390
|
+
const shapes = this.workspaceLayer.find('.rect, .text, .image, .pattern').filter((shape) => {
|
|
391
|
+
if (shape === this.canvasRect || shape === this.selectionRect || shape === this.transformer) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (!shape.visible() || !shape.listening()) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const layer = this._layers().find(l => l.id === shape.id());
|
|
398
|
+
if (layer?.locked) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
return Konva.Util.haveIntersection(box, shape.getClientRect());
|
|
402
|
+
});
|
|
403
|
+
this.transformer.nodes(shapes);
|
|
404
|
+
if (shapes.length === 1) {
|
|
405
|
+
const shape = shapes[0];
|
|
406
|
+
const layer = this._layers().find(l => l.id === shape.id());
|
|
407
|
+
this.updateTransformer(shape, !!layer?.locked);
|
|
408
|
+
this.selectedLayerId.set(shape.id());
|
|
409
|
+
}
|
|
410
|
+
else if (shapes.length > 1) {
|
|
411
|
+
// Multiple selection: show default anchors for all
|
|
412
|
+
this.transformer.setAttrs({
|
|
413
|
+
rotateEnabled: true,
|
|
414
|
+
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
|
|
415
|
+
});
|
|
416
|
+
this.selectedLayerId.set(null); // Or some multi-select id
|
|
417
|
+
}
|
|
418
|
+
this.transformer.forceUpdate();
|
|
419
|
+
this.updateSelectionBorders();
|
|
420
|
+
this.workspaceLayer.batchDraw();
|
|
421
|
+
};
|
|
422
|
+
handleGlobalMouseMove = (e) => {
|
|
423
|
+
if (!this.isSelecting || !this.selectionRect || !this.selectionStartPos || !this.stage) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const container = this.stage.container();
|
|
427
|
+
const rect = container.getBoundingClientRect();
|
|
428
|
+
let clientX, clientY;
|
|
429
|
+
if ('touches' in e) {
|
|
430
|
+
clientX = e.touches[0].clientX;
|
|
431
|
+
clientY = e.touches[0].clientY;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
clientX = e.clientX;
|
|
435
|
+
clientY = e.clientY;
|
|
436
|
+
}
|
|
437
|
+
const pos = {
|
|
438
|
+
x: clientX - rect.left,
|
|
439
|
+
y: clientY - rect.top
|
|
440
|
+
};
|
|
441
|
+
const rectX = Math.min(pos.x, this.selectionStartPos.x);
|
|
442
|
+
const rectY = Math.min(pos.y, this.selectionStartPos.y);
|
|
443
|
+
const rectWidth = Math.abs(pos.x - this.selectionStartPos.x);
|
|
444
|
+
const rectHeight = Math.abs(pos.y - this.selectionStartPos.y);
|
|
445
|
+
if (!isNaN(rectX) && !isNaN(rectY) && !isNaN(rectWidth) && !isNaN(rectHeight)) {
|
|
446
|
+
if (rectWidth > 5 || rectHeight > 5) {
|
|
447
|
+
this.selectionRect.visible(true);
|
|
448
|
+
}
|
|
449
|
+
this.selectionRect.setAttrs({
|
|
450
|
+
x: rectX,
|
|
451
|
+
y: rectY,
|
|
452
|
+
width: rectWidth,
|
|
453
|
+
height: rectHeight,
|
|
454
|
+
strokeWidth: 1,
|
|
455
|
+
});
|
|
456
|
+
this.selectionRect.moveToTop();
|
|
457
|
+
}
|
|
458
|
+
this.uiLayer?.moveToTop();
|
|
459
|
+
this.workspaceLayer?.batchDraw();
|
|
460
|
+
this.uiLayer?.batchDraw();
|
|
461
|
+
};
|
|
462
|
+
makeTextEditable(textNode) {
|
|
463
|
+
if (!this.stage)
|
|
464
|
+
return;
|
|
465
|
+
// Save original text and settings to restore if cancelled
|
|
466
|
+
textNode.setAttr('originalText', textNode.text());
|
|
467
|
+
textNode.setAttr('originalWidth', textNode.width());
|
|
468
|
+
// Fix width to current width during editing to enable wrapping
|
|
469
|
+
const initialWidth = textNode.width();
|
|
470
|
+
textNode.width(initialWidth);
|
|
471
|
+
// Hide text node but keep transformer visible
|
|
472
|
+
textNode.opacity(0);
|
|
473
|
+
this.transformer?.forceUpdate();
|
|
474
|
+
this.updateSelectionBorders();
|
|
475
|
+
const stage = this.stage;
|
|
476
|
+
const textPosition = textNode.getAbsolutePosition();
|
|
477
|
+
const stageBox = stage.container().getBoundingClientRect();
|
|
478
|
+
const areaPosition = {
|
|
479
|
+
x: stageBox.left + textPosition.x,
|
|
480
|
+
y: stageBox.top + textPosition.y,
|
|
481
|
+
};
|
|
482
|
+
// Create textarea
|
|
483
|
+
const textarea = document.createElement('textarea');
|
|
484
|
+
document.body.appendChild(textarea);
|
|
485
|
+
// Set styles
|
|
486
|
+
textarea.value = textNode.text();
|
|
487
|
+
textarea.style.position = 'absolute';
|
|
488
|
+
textarea.style.top = areaPosition.y + 'px';
|
|
489
|
+
textarea.style.left = areaPosition.x + 'px';
|
|
490
|
+
textarea.style.width = (textNode.width() * textNode.getAbsoluteScale().x) + 'px';
|
|
491
|
+
textarea.style.height = (textNode.height() * textNode.getAbsoluteScale().y + 10) + 'px';
|
|
492
|
+
textarea.style.fontSize = (textNode.fontSize() * textNode.getAbsoluteScale().y) + 'px';
|
|
493
|
+
textarea.style.border = 'none';
|
|
494
|
+
textarea.style.padding = (textNode.padding() * textNode.getAbsoluteScale().x) + 'px';
|
|
495
|
+
textarea.style.margin = '0px';
|
|
496
|
+
textarea.style.overflow = 'hidden';
|
|
497
|
+
textarea.style.background = 'none';
|
|
498
|
+
textarea.style.outline = 'none';
|
|
499
|
+
textarea.style.resize = 'none';
|
|
500
|
+
textarea.style.lineHeight = textNode.lineHeight().toString();
|
|
501
|
+
textarea.style.fontFamily = textNode.fontFamily();
|
|
502
|
+
textarea.style.fontWeight = (textNode.getAttr('fontWeight') || textNode.fontStyle() || 'normal').toString();
|
|
503
|
+
textarea.style.fontStyle = textNode.fontStyle()?.includes('italic') ? 'italic' : 'normal';
|
|
504
|
+
textarea.style.transformOrigin = 'left top';
|
|
505
|
+
textarea.style.textAlign = textNode.align();
|
|
506
|
+
textarea.style.color = textNode.fill();
|
|
507
|
+
textarea.style.boxSizing = 'border-box';
|
|
508
|
+
textarea.style.zIndex = '1000';
|
|
509
|
+
const rotation = textNode.getAbsoluteRotation();
|
|
510
|
+
let transform = '';
|
|
511
|
+
if (rotation) {
|
|
512
|
+
transform += 'rotateZ(' + rotation + 'deg)';
|
|
513
|
+
}
|
|
514
|
+
// Offset based on Konva's offset (textNode is centered by default in addLayer)
|
|
515
|
+
const px = 0;
|
|
516
|
+
// apply configuration
|
|
517
|
+
// we need to skip transform for now as it's tricky with absolute positioning and offsets
|
|
518
|
+
// But since we use absolutePosition which already accounts for many things, let's see.
|
|
519
|
+
// If textNode has offset (centered), we need to adjust textarea position
|
|
520
|
+
const offsetX = textNode.offsetX() * textNode.getAbsoluteScale().x;
|
|
521
|
+
const offsetY = textNode.offsetY() * textNode.getAbsoluteScale().y;
|
|
522
|
+
textarea.style.left = (areaPosition.x - offsetX) + 'px';
|
|
523
|
+
textarea.style.top = (areaPosition.y - offsetY) + 'px';
|
|
524
|
+
textarea.style.transform = transform;
|
|
525
|
+
textarea.focus();
|
|
526
|
+
let isFinished = false;
|
|
527
|
+
const removeTextarea = () => {
|
|
528
|
+
if (isFinished)
|
|
529
|
+
return;
|
|
530
|
+
isFinished = true;
|
|
531
|
+
const originalText = textNode.getAttr('originalText');
|
|
532
|
+
const originalWidth = textNode.getAttr('originalWidth');
|
|
533
|
+
if (originalText !== undefined) {
|
|
534
|
+
textNode.text(originalText);
|
|
535
|
+
}
|
|
536
|
+
if (originalWidth !== undefined) {
|
|
537
|
+
textNode.width(originalWidth);
|
|
538
|
+
}
|
|
539
|
+
if (textarea.parentNode) {
|
|
540
|
+
textarea.parentNode.removeChild(textarea);
|
|
541
|
+
}
|
|
542
|
+
window.removeEventListener('click', handleOutsideClick);
|
|
543
|
+
textarea.removeEventListener('blur', handleBlur);
|
|
544
|
+
textNode.opacity(1);
|
|
545
|
+
this.transformer?.forceUpdate();
|
|
546
|
+
this.updateSelectionBorders();
|
|
547
|
+
this.workspaceLayer?.batchDraw();
|
|
548
|
+
};
|
|
549
|
+
const updateText = () => {
|
|
550
|
+
const newText = textarea.value;
|
|
551
|
+
const originalText = textNode.getAttr('originalText');
|
|
552
|
+
if (newText !== originalText) {
|
|
553
|
+
const id = textNode.id();
|
|
554
|
+
if (id) {
|
|
555
|
+
// Keep the fixed width when updating the layer
|
|
556
|
+
this.updateLayer(id, { text: newText, width: textNode.width() });
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
textNode.text(newText);
|
|
560
|
+
textNode.offsetX(textNode.width() / 2);
|
|
561
|
+
textNode.offsetY(textNode.height() / 2);
|
|
562
|
+
this.workspaceLayer?.batchDraw();
|
|
563
|
+
}
|
|
564
|
+
// Restore transformer nodes after update if it was selected
|
|
565
|
+
if (this.selectedLayerId() === textNode.id()) {
|
|
566
|
+
this.transformer?.nodes([textNode]);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Clear originalText and originalWidth after update so removeTextarea doesn't restore it
|
|
570
|
+
textNode.setAttr('originalText', undefined);
|
|
571
|
+
textNode.setAttr('originalWidth', undefined);
|
|
572
|
+
};
|
|
573
|
+
const handleBlur = () => {
|
|
574
|
+
updateText();
|
|
575
|
+
removeTextarea();
|
|
576
|
+
};
|
|
577
|
+
textarea.addEventListener('blur', handleBlur);
|
|
578
|
+
textarea.addEventListener('keydown', (e) => {
|
|
579
|
+
// hide on enter
|
|
580
|
+
// but don't hide on shift + enter
|
|
581
|
+
if (e.keyCode === 13 && !e.shiftKey) {
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
textarea.blur();
|
|
584
|
+
}
|
|
585
|
+
// on escape do not update
|
|
586
|
+
if (e.keyCode === 27) {
|
|
587
|
+
removeTextarea();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
textarea.addEventListener('input', () => {
|
|
591
|
+
// update node text to update transformer
|
|
592
|
+
textNode.text(textarea.value);
|
|
593
|
+
this.transformer?.forceUpdate();
|
|
594
|
+
this.updateSelectionBorders();
|
|
595
|
+
// re-calculate size
|
|
596
|
+
// Width is fixed, only height should change
|
|
597
|
+
textarea.style.height = 'auto';
|
|
598
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
599
|
+
});
|
|
600
|
+
const handleOutsideClick = (e) => {
|
|
601
|
+
if (e.target !== textarea) {
|
|
602
|
+
// blur will handle the rest
|
|
603
|
+
textarea.blur();
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
// Use timeout to avoid immediate trigger from the same click that opened it
|
|
607
|
+
setTimeout(() => {
|
|
608
|
+
window.addEventListener('click', handleOutsideClick);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
setupSelectionListeners() {
|
|
612
|
+
if (!this.stage)
|
|
613
|
+
return;
|
|
614
|
+
if (this.isBrowser) {
|
|
615
|
+
window.addEventListener('mouseup', this.handleGlobalMouseUp);
|
|
616
|
+
window.addEventListener('touchend', this.handleGlobalMouseUp);
|
|
617
|
+
window.addEventListener('mousemove', this.handleGlobalMouseMove);
|
|
618
|
+
window.addEventListener('touchmove', this.handleGlobalMouseMove);
|
|
619
|
+
}
|
|
620
|
+
this.stage.on('mousedown touchstart', (e) => {
|
|
621
|
+
// If right click - do nothing
|
|
622
|
+
if (e.evt instanceof MouseEvent && e.evt.button === 2) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// If click on transformer - do nothing
|
|
626
|
+
const isTransformer = e.target.getParent()?.className === 'Transformer';
|
|
627
|
+
if (isTransformer) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// If click on empty area, canvasRect or a locked layer - start selection
|
|
631
|
+
const shapeAtPointer = e.target;
|
|
632
|
+
const layerConfig = this._layers().find(l => l.id === shapeAtPointer.id());
|
|
633
|
+
const isLocked = !!layerConfig?.locked;
|
|
634
|
+
if (e.target === this.stage || e.target === this.canvasRect || isLocked) {
|
|
635
|
+
this.isSelecting = true;
|
|
636
|
+
this.uiLayer?.moveToTop();
|
|
637
|
+
const pos = this.stage?.getPointerPosition();
|
|
638
|
+
if (pos) {
|
|
639
|
+
this.selectionStartPos = { x: pos.x, y: pos.y };
|
|
640
|
+
this.selectionRect?.visible(false);
|
|
641
|
+
this.selectionRect?.width(0);
|
|
642
|
+
this.selectionRect?.height(0);
|
|
643
|
+
this.selectionRect?.moveToTop();
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Find the clicked shape
|
|
648
|
+
const shape = e.target;
|
|
649
|
+
const id = shape.id();
|
|
650
|
+
if (id) {
|
|
651
|
+
const isShiftPressed = e.evt.shiftKey;
|
|
652
|
+
const currentNodes = this.transformer?.nodes() || [];
|
|
653
|
+
this.wasSelectedBeforeClick = currentNodes.includes(shape);
|
|
654
|
+
if (isShiftPressed) {
|
|
655
|
+
// Toggle selection
|
|
656
|
+
if (currentNodes.includes(shape)) {
|
|
657
|
+
const newNodes = currentNodes.filter(n => n !== shape);
|
|
658
|
+
this.transformer?.nodes(newNodes);
|
|
659
|
+
this.updateSelectionBorders();
|
|
660
|
+
if (newNodes.length === 1 && newNodes[0].id()) {
|
|
661
|
+
this.selectedLayerId.set(newNodes[0].id());
|
|
662
|
+
}
|
|
663
|
+
else if (newNodes.length === 0) {
|
|
664
|
+
this.selectedLayerId.set(null);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
this.selectedLayerId.set(null); // Multi-selection or none
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
const newNodes = [...currentNodes, shape];
|
|
672
|
+
this.transformer?.nodes(newNodes);
|
|
673
|
+
this.updateSelectionBorders();
|
|
674
|
+
if (newNodes.length === 1) {
|
|
675
|
+
this.selectedLayerId.set(shape.id());
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
this.selectedLayerId.set(null); // Multi-selection
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// Normal click
|
|
684
|
+
// Only change selection on mousedown if the object is NOT already selected.
|
|
685
|
+
// This allows moving the whole selection group.
|
|
686
|
+
if (!currentNodes.includes(shape)) {
|
|
687
|
+
this.selectLayer(id);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
// Click on something without ID (like the canvas but not the canvasRect handler above)
|
|
693
|
+
this.selectLayer(null);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
this.stage.on('click tap', (e) => {
|
|
697
|
+
// If right click - do nothing
|
|
698
|
+
if (e.evt instanceof MouseEvent && e.evt.button === 2) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// If click on transformer - do nothing
|
|
702
|
+
const isTransformer = e.target.getParent()?.className === 'Transformer';
|
|
703
|
+
if (isTransformer) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const shape = e.target;
|
|
707
|
+
const currentNodesOnStage = this.transformer?.nodes() || [];
|
|
708
|
+
if (this.wasSelectedBeforeClick && currentNodesOnStage.length === 1 && currentNodesOnStage[0] === shape && shape instanceof Konva.Text) {
|
|
709
|
+
this.makeTextEditable(shape);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// If click on empty area or canvasRect - do nothing
|
|
713
|
+
if (e.target === this.stage || e.target === this.canvasRect) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const id = shape.id();
|
|
717
|
+
if (!id)
|
|
718
|
+
return;
|
|
719
|
+
const isShiftPressed = e.evt.shiftKey;
|
|
720
|
+
if (isShiftPressed)
|
|
721
|
+
return; // Handled in mousedown
|
|
722
|
+
const currentNodesOnClick = this.transformer?.nodes() || [];
|
|
723
|
+
// If we clicked on an object that is part of a multi-selection,
|
|
724
|
+
// and it was a simple click (not a drag), then select only this object.
|
|
725
|
+
if (currentNodesOnClick.length > 1 && currentNodesOnClick.includes(shape)) {
|
|
726
|
+
this.selectLayer(id);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
this.stage.on('mousemove touchmove', (e) => {
|
|
730
|
+
// Logic moved to handleGlobalMouseMove
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
selectLayer(id) {
|
|
734
|
+
this.selectedLayerId.set(id);
|
|
735
|
+
this.hideHover();
|
|
736
|
+
if (!this.transformer || !this.workspaceLayer)
|
|
737
|
+
return;
|
|
738
|
+
if (!id) {
|
|
739
|
+
this.transformer.nodes([]);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
const shape = this.workspaceLayer.findOne('#' + id);
|
|
743
|
+
if (shape && shape.visible()) {
|
|
744
|
+
const layer = this._layers().find(l => l.id === id);
|
|
745
|
+
this.transformer.nodes([shape]);
|
|
746
|
+
this.updateTransformer(shape, !!layer?.locked);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
this.transformer.nodes([]);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
this.updateSelectionBorders();
|
|
753
|
+
this.transformer.moveToTop();
|
|
754
|
+
this.uiLayer?.moveToTop();
|
|
755
|
+
this.workspaceLayer.batchDraw();
|
|
756
|
+
this.uiLayer?.batchDraw();
|
|
757
|
+
}
|
|
758
|
+
updateTransformer(shape, isLocked) {
|
|
759
|
+
if (!this.transformer)
|
|
760
|
+
return;
|
|
761
|
+
if (isLocked) {
|
|
762
|
+
this.transformer.setAttrs({
|
|
763
|
+
rotateEnabled: false,
|
|
764
|
+
enabledAnchors: [],
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
if (shape instanceof Konva.Text) {
|
|
769
|
+
this.transformer.setAttrs({
|
|
770
|
+
rotateEnabled: true,
|
|
771
|
+
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right'],
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
this.transformer.setAttrs({
|
|
776
|
+
rotateEnabled: true,
|
|
777
|
+
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
this.transformer.forceUpdate();
|
|
782
|
+
}
|
|
783
|
+
toggleLayerVisibility(id) {
|
|
784
|
+
const layer = this._layers().find(l => l.id === id);
|
|
785
|
+
if (!layer || !this.workspaceLayer)
|
|
786
|
+
return;
|
|
787
|
+
const newVisible = layer.visible === false;
|
|
788
|
+
this._layers.update(layers => layers.map(l => l.id === id ? { ...l, visible: newVisible } : l));
|
|
789
|
+
const shape = this.workspaceLayer.findOne('#' + id);
|
|
790
|
+
if (shape) {
|
|
791
|
+
shape.visible(newVisible);
|
|
792
|
+
if (!newVisible && this.selectedLayerId() === id) {
|
|
793
|
+
this.selectLayer(null);
|
|
794
|
+
}
|
|
795
|
+
this.workspaceLayer.batchDraw();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
toggleLayerLock(id) {
|
|
799
|
+
const layer = this._layers().find(l => l.id === id);
|
|
800
|
+
if (!layer || !this.workspaceLayer)
|
|
801
|
+
return;
|
|
802
|
+
const newLocked = !layer.locked;
|
|
803
|
+
this._layers.update(layers => layers.map(l => l.id === id ? { ...l, locked: newLocked } : l));
|
|
804
|
+
const shape = this.workspaceLayer.findOne('#' + id);
|
|
805
|
+
if (shape) {
|
|
806
|
+
shape.listening(true);
|
|
807
|
+
shape.draggable(!newLocked);
|
|
808
|
+
// Update transformer if the locked shape is currently selected
|
|
809
|
+
if (this.selectedLayerId() === id) {
|
|
810
|
+
this.updateTransformer(shape, newLocked);
|
|
811
|
+
}
|
|
812
|
+
this.workspaceLayer.batchDraw();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async flipHorizontal(id) {
|
|
816
|
+
const layer = this._layers().find(l => l.id === id);
|
|
817
|
+
if (!layer)
|
|
818
|
+
return;
|
|
819
|
+
const currentScaleX = layer['scaleX'] ?? 1;
|
|
820
|
+
await this.updateLayer(id, { scaleX: currentScaleX * -1 });
|
|
821
|
+
}
|
|
822
|
+
async flipVertical(id) {
|
|
823
|
+
const layer = this._layers().find(l => l.id === id);
|
|
824
|
+
if (!layer)
|
|
825
|
+
return;
|
|
826
|
+
const currentScaleY = layer['scaleY'] ?? 1;
|
|
827
|
+
await this.updateLayer(id, { scaleY: currentScaleY * -1 });
|
|
828
|
+
}
|
|
829
|
+
async fitToPage(id) {
|
|
830
|
+
const layer = this._layers().find(l => l.id === id);
|
|
831
|
+
if (!layer || !this.canvasRect)
|
|
832
|
+
return;
|
|
833
|
+
const canvasWidth = this.canvasRect.width();
|
|
834
|
+
const canvasHeight = this.canvasRect.height();
|
|
835
|
+
const canvasCenterX = this.canvasRect.x();
|
|
836
|
+
const canvasCenterY = this.canvasRect.y();
|
|
837
|
+
const shape = this.workspaceLayer?.findOne('#' + id);
|
|
838
|
+
if (!shape)
|
|
839
|
+
return;
|
|
840
|
+
// Use actual shape dimensions if available, otherwise fallback to layer config
|
|
841
|
+
const layerWidth = (shape instanceof Konva.Text) ? shape.width() : (layer.width || shape.width());
|
|
842
|
+
const layerHeight = (shape instanceof Konva.Text) ? shape.height() : (layer.height || shape.height());
|
|
843
|
+
if (layerWidth === 0 || layerHeight === 0)
|
|
844
|
+
return;
|
|
845
|
+
const scale = Math.min(canvasWidth / layerWidth, canvasHeight / layerHeight) * this.scale();
|
|
846
|
+
await this.updateLayer(id, {
|
|
847
|
+
x: canvasCenterX,
|
|
848
|
+
y: canvasCenterY,
|
|
849
|
+
width: layerWidth,
|
|
850
|
+
height: layerHeight,
|
|
851
|
+
scaleX: scale,
|
|
852
|
+
scaleY: scale,
|
|
853
|
+
rotation: 0
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async fillPage(id) {
|
|
857
|
+
const layer = this._layers().find(l => l.id === id);
|
|
858
|
+
if (!layer || !this.canvasRect)
|
|
859
|
+
return;
|
|
860
|
+
const canvasWidth = this.canvasRect.width();
|
|
861
|
+
const canvasHeight = this.canvasRect.height();
|
|
862
|
+
const canvasCenterX = this.canvasRect.x();
|
|
863
|
+
const canvasCenterY = this.canvasRect.y();
|
|
864
|
+
const shape = this.workspaceLayer?.findOne('#' + id);
|
|
865
|
+
if (!shape)
|
|
866
|
+
return;
|
|
867
|
+
// Use actual shape dimensions if available, otherwise fallback to layer config
|
|
868
|
+
const layerWidth = (shape instanceof Konva.Text) ? shape.width() : (layer.width || shape.width());
|
|
869
|
+
const layerHeight = (shape instanceof Konva.Text) ? shape.height() : (layer.height || shape.height());
|
|
870
|
+
if (layerWidth === 0 || layerHeight === 0)
|
|
871
|
+
return;
|
|
872
|
+
const scale = Math.max(canvasWidth / layerWidth, canvasHeight / layerHeight) * this.scale();
|
|
873
|
+
await this.updateLayer(id, {
|
|
874
|
+
x: canvasCenterX,
|
|
875
|
+
y: canvasCenterY,
|
|
876
|
+
width: layerWidth,
|
|
877
|
+
height: layerHeight,
|
|
878
|
+
scaleX: scale,
|
|
879
|
+
scaleY: scale,
|
|
880
|
+
rotation: 0
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
deleteLayer(id) {
|
|
884
|
+
const layer = this._layers().find(l => l.id === id);
|
|
885
|
+
if (!layer || !this.workspaceLayer || layer.locked)
|
|
886
|
+
return;
|
|
887
|
+
this.hideHover();
|
|
888
|
+
// Remove from signal
|
|
889
|
+
this._layers.update(layers => layers.filter(l => l.id !== id));
|
|
890
|
+
this.notifyChange();
|
|
891
|
+
// Remove from Konva
|
|
892
|
+
const shape = this.workspaceLayer.findOne('#' + id);
|
|
893
|
+
if (shape) {
|
|
894
|
+
if (this.transformer) {
|
|
895
|
+
const nodes = this.transformer.nodes();
|
|
896
|
+
if (nodes.includes(shape)) {
|
|
897
|
+
this.transformer.nodes(nodes.filter(n => n !== shape));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
shape.destroy();
|
|
901
|
+
if (this.selectedLayerId() === id) {
|
|
902
|
+
this.selectLayer(null);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
this.updateSelectionBorders();
|
|
906
|
+
}
|
|
907
|
+
this.workspaceLayer.batchDraw();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
deleteSelectedLayers() {
|
|
911
|
+
if (!this.transformer || !this.workspaceLayer)
|
|
912
|
+
return;
|
|
913
|
+
const nodes = this.transformer.nodes();
|
|
914
|
+
if (nodes.length === 0)
|
|
915
|
+
return;
|
|
916
|
+
this.hideHover();
|
|
917
|
+
const idsToDelete = nodes.map(node => node.id()).filter((id) => {
|
|
918
|
+
const layer = this._layers().find(l => l.id === id);
|
|
919
|
+
return !!id && !!layer && !layer.locked;
|
|
920
|
+
});
|
|
921
|
+
if (idsToDelete.length === 0)
|
|
922
|
+
return;
|
|
923
|
+
// Remove from signal
|
|
924
|
+
this._layers.update(layers => layers.filter(l => !l.id || !idsToDelete.includes(l.id)));
|
|
925
|
+
this.notifyChange();
|
|
926
|
+
// Remove from Konva
|
|
927
|
+
idsToDelete.forEach(id => {
|
|
928
|
+
const shape = this.workspaceLayer?.findOne('#' + id);
|
|
929
|
+
if (shape) {
|
|
930
|
+
shape.destroy();
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
this.transformer.nodes([]);
|
|
934
|
+
this.selectLayer(null);
|
|
935
|
+
this.workspaceLayer.batchDraw();
|
|
936
|
+
}
|
|
937
|
+
moveSelectedLayers(dx, dy) {
|
|
938
|
+
if (!this.transformer || !this.workspaceLayer)
|
|
939
|
+
return;
|
|
940
|
+
const nodes = this.transformer.nodes();
|
|
941
|
+
if (nodes.length === 0)
|
|
942
|
+
return;
|
|
943
|
+
let moved = false;
|
|
944
|
+
nodes.forEach(node => {
|
|
945
|
+
const id = node.id();
|
|
946
|
+
const layer = this._layers().find(l => l.id === id);
|
|
947
|
+
if (layer && !layer.locked) {
|
|
948
|
+
node.x(node.x() + dx);
|
|
949
|
+
node.y(node.y() + dy);
|
|
950
|
+
moved = true;
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
if (moved) {
|
|
954
|
+
this.updateSelectionBorders();
|
|
955
|
+
this.workspaceLayer.batchDraw();
|
|
956
|
+
this.updateShapesOriginalPositions(nodes);
|
|
957
|
+
this.notifyChange();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
showHover(shape) {
|
|
961
|
+
if (!this.hoverRect || !this.workspaceLayer)
|
|
962
|
+
return;
|
|
963
|
+
this.hoveredShape = shape;
|
|
964
|
+
const box = shape.getClientRect({ relativeTo: this.workspaceLayer });
|
|
965
|
+
this.hoverRect.setAttrs({
|
|
966
|
+
x: box.x,
|
|
967
|
+
y: box.y,
|
|
968
|
+
width: box.width,
|
|
969
|
+
height: box.height,
|
|
970
|
+
strokeWidth: 1,
|
|
971
|
+
visible: true
|
|
972
|
+
});
|
|
973
|
+
this.hoverRect.moveToTop();
|
|
974
|
+
this.workspaceLayer.batchDraw();
|
|
975
|
+
}
|
|
976
|
+
hideHover() {
|
|
977
|
+
this.hoveredShape = undefined;
|
|
978
|
+
if (!this.hoverRect)
|
|
979
|
+
return;
|
|
980
|
+
this.hoverRect.visible(false);
|
|
981
|
+
this.workspaceLayer?.batchDraw();
|
|
982
|
+
}
|
|
983
|
+
updateSelectionBorders() {
|
|
984
|
+
if (!this.selectionBordersGroup || !this.transformer || !this.workspaceLayer)
|
|
985
|
+
return;
|
|
986
|
+
this.selectionBordersGroup.destroyChildren();
|
|
987
|
+
const nodes = this.transformer.nodes();
|
|
988
|
+
if (nodes.length > 1) {
|
|
989
|
+
nodes.forEach(node => {
|
|
990
|
+
const box = node.getClientRect({ relativeTo: this.workspaceLayer });
|
|
991
|
+
const rect = new Konva.Rect({
|
|
992
|
+
x: box.x,
|
|
993
|
+
y: box.y,
|
|
994
|
+
width: box.width,
|
|
995
|
+
height: box.height,
|
|
996
|
+
stroke: '#3b82f6',
|
|
997
|
+
strokeWidth: 1,
|
|
998
|
+
listening: false,
|
|
999
|
+
name: 'selectionBorder'
|
|
1000
|
+
});
|
|
1001
|
+
this.selectionBordersGroup?.add(rect);
|
|
1002
|
+
});
|
|
1003
|
+
this.selectionBordersGroup.visible(true);
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
this.selectionBordersGroup.visible(false);
|
|
1007
|
+
}
|
|
1008
|
+
this.selectionBordersGroup.moveToTop();
|
|
1009
|
+
this.uiLayer?.batchDraw();
|
|
1010
|
+
}
|
|
1011
|
+
async addLayer(config, updateList = true) {
|
|
1012
|
+
if (!this.workspaceLayer || !this.canvasRect)
|
|
1013
|
+
return undefined;
|
|
1014
|
+
if (config.type === 'text' && config['fontFamily']) {
|
|
1015
|
+
await this.loadFont(config['fontFamily']);
|
|
1016
|
+
}
|
|
1017
|
+
else if (config.type === 'text' && !config['fontFamily']) {
|
|
1018
|
+
await this.loadFont(this.defaultFont);
|
|
1019
|
+
}
|
|
1020
|
+
if (updateList) {
|
|
1021
|
+
const newConfig = { ...config, id: config.id || Math.random().toString(36).substr(2, 9) };
|
|
1022
|
+
// New layer should be at the top of the UI list (index 0)
|
|
1023
|
+
this._layers.update(layers => [newConfig, ...layers]);
|
|
1024
|
+
config = newConfig;
|
|
1025
|
+
if (!this.isSnapshotLoading) {
|
|
1026
|
+
this.notifyChange();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
let shape;
|
|
1030
|
+
const canvasWidth = this.canvasRect?.width() || 0;
|
|
1031
|
+
const canvasHeight = this.canvasRect?.height() || 0;
|
|
1032
|
+
const canvasX = this.canvasRect?.x() || 0;
|
|
1033
|
+
const canvasY = this.canvasRect?.y() || 0;
|
|
1034
|
+
// x and y are relative to canvas center
|
|
1035
|
+
const x = (config.x ?? 0) * this.scale() + canvasX;
|
|
1036
|
+
const y = (config.y ?? 0) * this.scale() + canvasY;
|
|
1037
|
+
if (isNaN(x) || isNaN(y)) {
|
|
1038
|
+
console.warn('ImageDesignerService.addLayer: Calculated layer position is NaN', { x, y, config, canvasWidth, canvasHeight, canvasX, canvasY, scale: this.scale() });
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
if (config.type === 'text') {
|
|
1042
|
+
const fontSize = config['fontSize'] || 18;
|
|
1043
|
+
const padding = 10;
|
|
1044
|
+
shape = new Konva.Text({
|
|
1045
|
+
id: config.id,
|
|
1046
|
+
name: 'text',
|
|
1047
|
+
x,
|
|
1048
|
+
y,
|
|
1049
|
+
text: config['text'] || 'New Text',
|
|
1050
|
+
fontSize,
|
|
1051
|
+
fontFamily: config['fontFamily'] || this.defaultFont,
|
|
1052
|
+
fontStyle: config['fontWeight'] ? `${config['fontWeight']}` : 'normal',
|
|
1053
|
+
fill: config['fill'] || '#000000',
|
|
1054
|
+
opacity: config['opacity'] ?? 1,
|
|
1055
|
+
align: config['align'] || 'left',
|
|
1056
|
+
scaleX: this.scale(),
|
|
1057
|
+
scaleY: this.scale(),
|
|
1058
|
+
draggable: !config.locked,
|
|
1059
|
+
listening: true,
|
|
1060
|
+
visible: config.visible !== false,
|
|
1061
|
+
padding: padding,
|
|
1062
|
+
wrap: 'word',
|
|
1063
|
+
width: config.width || undefined,
|
|
1064
|
+
});
|
|
1065
|
+
if (config['fontWeight']) {
|
|
1066
|
+
shape.setAttr('fontWeight', config['fontWeight']);
|
|
1067
|
+
}
|
|
1068
|
+
// If width is not provided, let's set a default width for headings to enable wrapping/resizing properly
|
|
1069
|
+
if (!config.width) {
|
|
1070
|
+
// Measure text width and add extra horizontal padding
|
|
1071
|
+
const textWidth = shape.width() + 55;
|
|
1072
|
+
shape.width(textWidth);
|
|
1073
|
+
}
|
|
1074
|
+
shape.offsetX(shape.width() / 2);
|
|
1075
|
+
shape.offsetY(shape.height() / 2);
|
|
1076
|
+
this.applyEffectsToShape(shape, config);
|
|
1077
|
+
}
|
|
1078
|
+
else if (config.type === 'image') {
|
|
1079
|
+
const imageObj = new Image();
|
|
1080
|
+
imageObj.crossOrigin = 'anonymous';
|
|
1081
|
+
imageObj.onload = () => {
|
|
1082
|
+
if (shape) {
|
|
1083
|
+
shape.image(imageObj);
|
|
1084
|
+
if (!config.width || !config.height) {
|
|
1085
|
+
shape.width(imageObj.width);
|
|
1086
|
+
shape.height(imageObj.height);
|
|
1087
|
+
}
|
|
1088
|
+
shape.offsetX(shape.width() / 2);
|
|
1089
|
+
shape.offsetY(shape.height() / 2);
|
|
1090
|
+
if (this.transformer?.nodes().includes(shape)) {
|
|
1091
|
+
this.transformer.forceUpdate();
|
|
1092
|
+
}
|
|
1093
|
+
this.applyEffectsToShape(shape, config);
|
|
1094
|
+
this.workspaceLayer?.batchDraw();
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
imageObj.src = config['src'] || '';
|
|
1098
|
+
shape = new Konva.Image({
|
|
1099
|
+
id: config.id,
|
|
1100
|
+
name: 'image',
|
|
1101
|
+
x,
|
|
1102
|
+
y,
|
|
1103
|
+
width: config.width || 0,
|
|
1104
|
+
height: config.height || 0,
|
|
1105
|
+
image: undefined,
|
|
1106
|
+
opacity: config['opacity'] ?? 1,
|
|
1107
|
+
scaleX: this.scale(),
|
|
1108
|
+
scaleY: this.scale(),
|
|
1109
|
+
draggable: !config.locked,
|
|
1110
|
+
listening: true,
|
|
1111
|
+
visible: config.visible !== false,
|
|
1112
|
+
});
|
|
1113
|
+
shape.offsetX(shape.width() / 2);
|
|
1114
|
+
shape.offsetY(shape.height() / 2);
|
|
1115
|
+
}
|
|
1116
|
+
else if (config.type === 'shape') {
|
|
1117
|
+
const imageObj = new Image();
|
|
1118
|
+
imageObj.onload = () => {
|
|
1119
|
+
if (shape) {
|
|
1120
|
+
shape.image(imageObj);
|
|
1121
|
+
shape.offsetX(shape.width() / 2);
|
|
1122
|
+
shape.offsetY(shape.height() / 2);
|
|
1123
|
+
if (this.transformer?.nodes().includes(shape)) {
|
|
1124
|
+
this.transformer.forceUpdate();
|
|
1125
|
+
}
|
|
1126
|
+
this.applyEffectsToShape(shape, config);
|
|
1127
|
+
this.workspaceLayer?.batchDraw();
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
imageObj.src = config['data'] || '';
|
|
1131
|
+
shape = new Konva.Image({
|
|
1132
|
+
id: config.id,
|
|
1133
|
+
name: 'rect',
|
|
1134
|
+
x,
|
|
1135
|
+
y,
|
|
1136
|
+
width: config.width || 100,
|
|
1137
|
+
height: config.height || 100,
|
|
1138
|
+
image: undefined,
|
|
1139
|
+
opacity: config['opacity'] ?? 1,
|
|
1140
|
+
scaleX: this.scale(),
|
|
1141
|
+
scaleY: this.scale(),
|
|
1142
|
+
draggable: !config.locked,
|
|
1143
|
+
listening: true,
|
|
1144
|
+
visible: config.visible !== false,
|
|
1145
|
+
});
|
|
1146
|
+
shape.offsetX(shape.width() / 2);
|
|
1147
|
+
shape.offsetY(shape.height() / 2);
|
|
1148
|
+
}
|
|
1149
|
+
else if (config.type === 'pattern') {
|
|
1150
|
+
const patternImageObj = new Image();
|
|
1151
|
+
patternImageObj.onload = () => {
|
|
1152
|
+
if (shape) {
|
|
1153
|
+
const rect = shape;
|
|
1154
|
+
rect.fillPatternImage(patternImageObj);
|
|
1155
|
+
// If original pattern is too big, scale it down.
|
|
1156
|
+
// Or just provide a default scale for better look.
|
|
1157
|
+
const patternScale = 0.5; // Fixed pattern scale for better look or based on config
|
|
1158
|
+
rect.fillPatternScale({ x: patternScale, y: patternScale });
|
|
1159
|
+
this.workspaceLayer?.batchDraw();
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
patternImageObj.src = typeof config.patternImage === 'string' ? config.patternImage : (config.patternImage?.src || '');
|
|
1163
|
+
shape = new Konva.Rect({
|
|
1164
|
+
id: config.id,
|
|
1165
|
+
name: 'pattern',
|
|
1166
|
+
x,
|
|
1167
|
+
y,
|
|
1168
|
+
width: config.width || canvasWidth,
|
|
1169
|
+
height: config.height || canvasHeight,
|
|
1170
|
+
opacity: config['opacity'] ?? 1,
|
|
1171
|
+
scaleX: this.scale(),
|
|
1172
|
+
scaleY: this.scale(),
|
|
1173
|
+
draggable: !config.locked,
|
|
1174
|
+
listening: true,
|
|
1175
|
+
visible: config.visible !== false,
|
|
1176
|
+
fillPatternRepeat: 'repeat',
|
|
1177
|
+
});
|
|
1178
|
+
shape.offsetX(shape.width() / 2);
|
|
1179
|
+
shape.offsetY(shape.height() / 2);
|
|
1180
|
+
// Ensure pattern stays centered even if shape size changes
|
|
1181
|
+
// Konva's fillPatternOffset is in local coordinates of the shape (after scale/offset?)
|
|
1182
|
+
// Actually it's simpler: if we want it centered, we just need to keep it consistent.
|
|
1183
|
+
}
|
|
1184
|
+
if (shape) {
|
|
1185
|
+
if (config.id) {
|
|
1186
|
+
shape.id(config.id);
|
|
1187
|
+
}
|
|
1188
|
+
this.workspaceLayer.add(shape);
|
|
1189
|
+
// Select the new layer automatically
|
|
1190
|
+
if (updateList && config.id) {
|
|
1191
|
+
this.selectLayer(config.id);
|
|
1192
|
+
}
|
|
1193
|
+
// Update Konva Z-index based on the array order
|
|
1194
|
+
if (updateList) {
|
|
1195
|
+
this.reorderLayers(0, 0); // This will refresh all Z-indices based on the current _layers()
|
|
1196
|
+
}
|
|
1197
|
+
this.workspaceLayer.batchDraw();
|
|
1198
|
+
shape.on('mouseenter', () => {
|
|
1199
|
+
if (this.isDragging || this.isSelecting || !this.hoverRect)
|
|
1200
|
+
return;
|
|
1201
|
+
// Don't show hover for already selected shapes
|
|
1202
|
+
const selectedNodes = this.transformer?.nodes() || [];
|
|
1203
|
+
if (selectedNodes.includes(shape))
|
|
1204
|
+
return;
|
|
1205
|
+
this.showHover(shape);
|
|
1206
|
+
});
|
|
1207
|
+
shape.on('mouseleave', () => {
|
|
1208
|
+
this.hideHover();
|
|
1209
|
+
});
|
|
1210
|
+
shape.dragBoundFunc((pos) => {
|
|
1211
|
+
const nodes = this.transformer?.nodes() || [];
|
|
1212
|
+
if (nodes.length > 1 && nodes.includes(shape)) {
|
|
1213
|
+
return pos;
|
|
1214
|
+
}
|
|
1215
|
+
const guides = this.getSnappingGuides(shape, pos);
|
|
1216
|
+
let x = pos.x;
|
|
1217
|
+
let y = pos.y;
|
|
1218
|
+
if (guides.vertical.length > 0) {
|
|
1219
|
+
const vGuide = guides.vertical[0];
|
|
1220
|
+
x = vGuide.guide - vGuide.offset + shape.offsetX() * shape.scaleX();
|
|
1221
|
+
}
|
|
1222
|
+
if (guides.horizontal.length > 0) {
|
|
1223
|
+
const hGuide = guides.horizontal[0];
|
|
1224
|
+
y = hGuide.guide - hGuide.offset + shape.offsetY() * shape.scaleY();
|
|
1225
|
+
}
|
|
1226
|
+
return { x, y };
|
|
1227
|
+
});
|
|
1228
|
+
shape.on('dragstart', () => {
|
|
1229
|
+
this.isDragging = true;
|
|
1230
|
+
this.hideHover();
|
|
1231
|
+
});
|
|
1232
|
+
shape.on('dragmove', (e) => {
|
|
1233
|
+
const nodes = this.transformer?.nodes() || [];
|
|
1234
|
+
if (nodes.length > 1 && nodes.includes(shape)) {
|
|
1235
|
+
// If part of group, let transformer.on('dragmove') handle it
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (nodes.includes(shape) && nodes.length > 1) {
|
|
1239
|
+
nodes.forEach(node => {
|
|
1240
|
+
this.updateShapeOriginalPosition(node, false);
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
this.updateShapeOriginalPosition(shape, false);
|
|
1245
|
+
}
|
|
1246
|
+
const absPos = shape.getAbsolutePosition();
|
|
1247
|
+
const guides = this.getSnappingGuides(shape, absPos);
|
|
1248
|
+
this.drawSnappingLines(guides);
|
|
1249
|
+
});
|
|
1250
|
+
shape.on('dragend', () => {
|
|
1251
|
+
this.isDragging = false;
|
|
1252
|
+
this.clearSnapLines();
|
|
1253
|
+
const nodes = this.transformer?.nodes() || [];
|
|
1254
|
+
if (nodes.includes(shape) && nodes.length > 1) {
|
|
1255
|
+
// Batch update signal for all nodes in the group
|
|
1256
|
+
this.updateShapesOriginalPositions(nodes);
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
// Single shape drag - update signal
|
|
1260
|
+
this.updateShapeOriginalPosition(shape);
|
|
1261
|
+
}
|
|
1262
|
+
this.notifyChange();
|
|
1263
|
+
});
|
|
1264
|
+
shape.on('transform', () => {
|
|
1265
|
+
if (shape.name() === 'pattern') {
|
|
1266
|
+
const rect = shape;
|
|
1267
|
+
const scaleX = rect.scaleX();
|
|
1268
|
+
const scaleY = rect.scaleY();
|
|
1269
|
+
rect.width(rect.width() * (scaleX / this.scale()));
|
|
1270
|
+
rect.height(rect.height() * (scaleY / this.scale()));
|
|
1271
|
+
rect.scaleX(this.scale());
|
|
1272
|
+
rect.scaleY(this.scale());
|
|
1273
|
+
rect.offsetX(rect.width() / 2);
|
|
1274
|
+
rect.offsetY(rect.height() / 2);
|
|
1275
|
+
}
|
|
1276
|
+
const absPos = shape.getAbsolutePosition();
|
|
1277
|
+
const guides = this.getSnappingGuides(shape, absPos);
|
|
1278
|
+
this.drawSnappingLines(guides);
|
|
1279
|
+
this.updateShapeOriginalPosition(shape);
|
|
1280
|
+
});
|
|
1281
|
+
shape.on('transformend', () => {
|
|
1282
|
+
this.clearSnapLines();
|
|
1283
|
+
if (!(shape instanceof Konva.Text)) {
|
|
1284
|
+
const scaleX = shape.scaleX();
|
|
1285
|
+
const scaleY = shape.scaleY();
|
|
1286
|
+
shape.width(shape.width() * (scaleX / this.scale()));
|
|
1287
|
+
shape.height(shape.height() * (scaleY / this.scale()));
|
|
1288
|
+
shape.scaleX(this.scale());
|
|
1289
|
+
shape.scaleY(this.scale());
|
|
1290
|
+
shape.offsetX(shape.width() / 2);
|
|
1291
|
+
shape.offsetY(shape.height() / 2);
|
|
1292
|
+
}
|
|
1293
|
+
this.updateShapeOriginalPosition(shape);
|
|
1294
|
+
this.notifyChange();
|
|
1295
|
+
});
|
|
1296
|
+
shape.setAttr('originalX', config.x ?? 0);
|
|
1297
|
+
shape.setAttr('originalY', config.y ?? 0);
|
|
1298
|
+
// Ensure UI layer and its elements are on top
|
|
1299
|
+
this.uiLayer?.moveToTop();
|
|
1300
|
+
this.maskOverlay?.moveToTop();
|
|
1301
|
+
this.transformer?.moveToTop();
|
|
1302
|
+
this.hoverRect?.moveToTop();
|
|
1303
|
+
}
|
|
1304
|
+
return config.id;
|
|
1305
|
+
}
|
|
1306
|
+
updateShapeOriginalPosition(shape, updateSignal = true) {
|
|
1307
|
+
if (!this.canvasRect)
|
|
1308
|
+
return;
|
|
1309
|
+
const scale = this.scale();
|
|
1310
|
+
const originalX = (shape.x() - this.canvasRect.x()) / scale;
|
|
1311
|
+
const originalY = (shape.y() - this.canvasRect.y()) / scale;
|
|
1312
|
+
const originalScaleX = shape.scaleX() / scale;
|
|
1313
|
+
const originalScaleY = shape.scaleY() / scale;
|
|
1314
|
+
shape.setAttr('originalX', originalX);
|
|
1315
|
+
shape.setAttr('originalY', originalY);
|
|
1316
|
+
shape.setAttr('originalScaleX', originalScaleX);
|
|
1317
|
+
shape.setAttr('originalScaleY', originalScaleY);
|
|
1318
|
+
if (updateSignal) {
|
|
1319
|
+
// Update the signal
|
|
1320
|
+
const id = shape.id();
|
|
1321
|
+
this._layers.update(layers => layers.map(l => l.id === id ? {
|
|
1322
|
+
...l,
|
|
1323
|
+
x: originalX,
|
|
1324
|
+
y: originalY,
|
|
1325
|
+
width: shape.width(),
|
|
1326
|
+
height: shape.height(),
|
|
1327
|
+
scaleX: originalScaleX,
|
|
1328
|
+
scaleY: originalScaleY,
|
|
1329
|
+
rotation: shape.rotation()
|
|
1330
|
+
} : l));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
updateShapesOriginalPositions(shapes) {
|
|
1334
|
+
if (!this.canvasRect)
|
|
1335
|
+
return;
|
|
1336
|
+
const scale = this.scale();
|
|
1337
|
+
const updates = new Map();
|
|
1338
|
+
shapes.forEach(shape => {
|
|
1339
|
+
const originalX = (shape.x() - this.canvasRect.x()) / scale;
|
|
1340
|
+
const originalY = (shape.y() - this.canvasRect.y()) / scale;
|
|
1341
|
+
const originalScaleX = shape.scaleX() / scale;
|
|
1342
|
+
const originalScaleY = shape.scaleY() / scale;
|
|
1343
|
+
shape.setAttr('originalX', originalX);
|
|
1344
|
+
shape.setAttr('originalY', originalY);
|
|
1345
|
+
shape.setAttr('originalScaleX', originalScaleX);
|
|
1346
|
+
shape.setAttr('originalScaleY', originalScaleY);
|
|
1347
|
+
updates.set(shape.id(), {
|
|
1348
|
+
x: originalX,
|
|
1349
|
+
y: originalY,
|
|
1350
|
+
width: shape.width(),
|
|
1351
|
+
height: shape.height(),
|
|
1352
|
+
scaleX: originalScaleX,
|
|
1353
|
+
scaleY: originalScaleY,
|
|
1354
|
+
rotation: shape.rotation()
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
// Update the signal once for all shapes
|
|
1358
|
+
this._layers.update(layers => layers.map(l => {
|
|
1359
|
+
const update = updates.get(l.id);
|
|
1360
|
+
return update ? { ...l, ...update } : l;
|
|
1361
|
+
}));
|
|
1362
|
+
}
|
|
1363
|
+
getNodesClientRect(nodes) {
|
|
1364
|
+
if (nodes.length === 0)
|
|
1365
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1366
|
+
let minX = Infinity;
|
|
1367
|
+
let minY = Infinity;
|
|
1368
|
+
let maxX = -Infinity;
|
|
1369
|
+
let maxY = -Infinity;
|
|
1370
|
+
nodes.forEach(node => {
|
|
1371
|
+
const box = node.getClientRect();
|
|
1372
|
+
minX = Math.min(minX, box.x);
|
|
1373
|
+
minY = Math.min(minY, box.y);
|
|
1374
|
+
maxX = Math.max(maxX, box.x + box.width);
|
|
1375
|
+
maxY = Math.max(maxY, box.y + box.height);
|
|
1376
|
+
});
|
|
1377
|
+
return {
|
|
1378
|
+
x: minX,
|
|
1379
|
+
y: minY,
|
|
1380
|
+
width: maxX - minX,
|
|
1381
|
+
height: maxY - minY
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
getSnappingGuides(targetNode, pos) {
|
|
1385
|
+
if (!this.canvasRect || !this.workspaceLayer)
|
|
1386
|
+
return { vertical: [], horizontal: [] };
|
|
1387
|
+
const nodes = Array.isArray(targetNode) ? targetNode : [targetNode];
|
|
1388
|
+
const isGroup = nodes.length > 1;
|
|
1389
|
+
const snapRange = this.snapSettings.snapRange;
|
|
1390
|
+
const resultV = [];
|
|
1391
|
+
const resultH = [];
|
|
1392
|
+
// Bases for snapping
|
|
1393
|
+
const basesV = [];
|
|
1394
|
+
const basesH = [];
|
|
1395
|
+
// 1. Stage centers and borders
|
|
1396
|
+
if (this.snapSettings.snapToStageCenter || this.snapSettings.snapToStageBorders) {
|
|
1397
|
+
const cw = this.canvasRect.width() * this.scale();
|
|
1398
|
+
const ch = this.canvasRect.height() * this.scale();
|
|
1399
|
+
const cx = this.canvasRect.x();
|
|
1400
|
+
const cy = this.canvasRect.y();
|
|
1401
|
+
if (this.snapSettings.snapToStageBorders) {
|
|
1402
|
+
basesV.push(cx); // Left
|
|
1403
|
+
basesV.push(cx + cw); // Right
|
|
1404
|
+
basesH.push(cy); // Top
|
|
1405
|
+
basesH.push(cy + ch); // Bottom
|
|
1406
|
+
}
|
|
1407
|
+
if (this.snapSettings.snapToStageCenter) {
|
|
1408
|
+
basesV.push(cx + cw / 2); // Center V
|
|
1409
|
+
basesH.push(cy + ch / 2); // Center H
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
// 2. Other shapes
|
|
1413
|
+
if (this.snapSettings.snapToShapes) {
|
|
1414
|
+
const selectedNodes = this.transformer?.nodes() || [];
|
|
1415
|
+
this.workspaceLayer.getChildren().forEach((child) => {
|
|
1416
|
+
if (nodes.includes(child) || child === this.canvasRect || child.name() === 'selection-rect' || (selectedNodes.length > 0 && selectedNodes.includes(child)))
|
|
1417
|
+
return;
|
|
1418
|
+
const box = child.getClientRect();
|
|
1419
|
+
basesV.push(box.x);
|
|
1420
|
+
basesV.push(box.x + box.width / 2);
|
|
1421
|
+
basesV.push(box.x + box.width);
|
|
1422
|
+
basesH.push(box.y);
|
|
1423
|
+
basesH.push(box.y + box.height / 2);
|
|
1424
|
+
basesH.push(box.y + box.height);
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
const targetBox = isGroup ? this.getNodesClientRect(nodes) : nodes[0].getClientRect();
|
|
1428
|
+
// If pos is provided (from dragBoundFunc for single node), we use it.
|
|
1429
|
+
// For group drag via transformer, we don't use pos here because we apply delta later.
|
|
1430
|
+
let targetX = targetBox.x;
|
|
1431
|
+
let targetY = targetBox.y;
|
|
1432
|
+
if (!isGroup && pos) {
|
|
1433
|
+
targetX = pos.x - nodes[0].offsetX() * nodes[0].scaleX();
|
|
1434
|
+
targetY = pos.y - nodes[0].offsetY() * nodes[0].scaleY();
|
|
1435
|
+
}
|
|
1436
|
+
const targetWidth = targetBox.width;
|
|
1437
|
+
const targetHeight = targetBox.height;
|
|
1438
|
+
basesV.forEach((base) => {
|
|
1439
|
+
const vSnaps = [
|
|
1440
|
+
{ guide: base, offset: 0, diff: Math.abs(base - targetX) },
|
|
1441
|
+
{ guide: base, offset: targetWidth / 2, diff: Math.abs(base - (targetX + targetWidth / 2)) },
|
|
1442
|
+
{ guide: base, offset: targetWidth, diff: Math.abs(base - (targetX + targetWidth)) }
|
|
1443
|
+
];
|
|
1444
|
+
vSnaps.forEach(s => {
|
|
1445
|
+
if (s.diff < snapRange) {
|
|
1446
|
+
resultV.push(s);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
basesH.forEach((base) => {
|
|
1451
|
+
const hSnaps = [
|
|
1452
|
+
{ guide: base, offset: 0, diff: Math.abs(base - targetY) },
|
|
1453
|
+
{ guide: base, offset: targetHeight / 2, diff: Math.abs(base - (targetY + targetHeight / 2)) },
|
|
1454
|
+
{ guide: base, offset: targetHeight, diff: Math.abs(base - (targetY + targetHeight)) }
|
|
1455
|
+
];
|
|
1456
|
+
hSnaps.forEach(s => {
|
|
1457
|
+
if (s.diff < snapRange) {
|
|
1458
|
+
resultH.push(s);
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
return {
|
|
1463
|
+
vertical: resultV.sort((a, b) => a.diff - b.diff),
|
|
1464
|
+
horizontal: resultH.sort((a, b) => a.diff - b.diff)
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
drawSnappingLines(guides) {
|
|
1468
|
+
this.clearSnapLines();
|
|
1469
|
+
if (!this.uiLayer || !this.snapSettings.showGuidelines)
|
|
1470
|
+
return;
|
|
1471
|
+
const vGuide = guides.vertical[0];
|
|
1472
|
+
const hGuide = guides.horizontal[0];
|
|
1473
|
+
if (vGuide) {
|
|
1474
|
+
const line = new Konva.Line({
|
|
1475
|
+
points: [vGuide.guide, 0, vGuide.guide, this.stage.height()],
|
|
1476
|
+
stroke: this.snapSettings.guidelineColor,
|
|
1477
|
+
strokeWidth: 1,
|
|
1478
|
+
dash: [4, 6],
|
|
1479
|
+
name: 'snap-line'
|
|
1480
|
+
});
|
|
1481
|
+
this.uiLayer.add(line);
|
|
1482
|
+
this.snapLines.push(line);
|
|
1483
|
+
}
|
|
1484
|
+
if (hGuide) {
|
|
1485
|
+
const line = new Konva.Line({
|
|
1486
|
+
points: [0, hGuide.guide, this.stage.width(), hGuide.guide],
|
|
1487
|
+
stroke: this.snapSettings.guidelineColor,
|
|
1488
|
+
strokeWidth: 1,
|
|
1489
|
+
dash: [4, 6],
|
|
1490
|
+
name: 'snap-line'
|
|
1491
|
+
});
|
|
1492
|
+
this.uiLayer.add(line);
|
|
1493
|
+
this.snapLines.push(line);
|
|
1494
|
+
}
|
|
1495
|
+
this.uiLayer.batchDraw();
|
|
1496
|
+
}
|
|
1497
|
+
clearSnapLines() {
|
|
1498
|
+
this.snapLines.forEach(l => l.destroy());
|
|
1499
|
+
this.snapLines = [];
|
|
1500
|
+
this.uiLayer?.batchDraw();
|
|
1501
|
+
}
|
|
1502
|
+
consecutiveUpdatesCount = 0;
|
|
1503
|
+
lastUpdateSizeTime = 0;
|
|
1504
|
+
lastUpdateSizeDimensions = { width: 0, height: 0 };
|
|
1505
|
+
isSnapshotLoading = false;
|
|
1506
|
+
setupResizeObserver(container, imageSize) {
|
|
1507
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
1508
|
+
// Use requestAnimationFrame to avoid "ResizeObserver loop completed with undelivered notifications" error
|
|
1509
|
+
// This ensures that the size update happens in the next frame after the layout has settled.
|
|
1510
|
+
requestAnimationFrame(() => {
|
|
1511
|
+
if (this.stage) {
|
|
1512
|
+
this.updateSize(container, imageSize);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
this.resizeObserver.observe(container);
|
|
1517
|
+
}
|
|
1518
|
+
updateSize(container, imageSize, fitToContainer = false, notify = false) {
|
|
1519
|
+
if (!this.stage || !this.canvasRect || !this.workspaceLayer || !this.uiLayer) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const now = Date.now();
|
|
1523
|
+
if (this.lastUpdateSizeTime && now - this.lastUpdateSizeTime < 50) {
|
|
1524
|
+
this.consecutiveUpdatesCount++;
|
|
1525
|
+
if (this.consecutiveUpdatesCount > 100) {
|
|
1526
|
+
console.warn('ImageDesignerService.updateSize: Potential infinite loop detected, aborting');
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
this.consecutiveUpdatesCount = 0;
|
|
1532
|
+
}
|
|
1533
|
+
this.lastUpdateSizeTime = now;
|
|
1534
|
+
this.lastUpdateSizeDimensions = { width: imageSize.width, height: imageSize.height };
|
|
1535
|
+
const width = Math.round(container.offsetWidth || imageSize.width || 800);
|
|
1536
|
+
const height = Math.round(container.offsetHeight || imageSize.height || 600);
|
|
1537
|
+
console.log('ImageDesignerService.updateSize dimensions:', width, 'x', height);
|
|
1538
|
+
// Skip if size is effectively 0 to avoid incorrect centering
|
|
1539
|
+
if (width === 0 || height === 0) {
|
|
1540
|
+
console.warn('ImageDesignerService.updateSize: dimensions are 0, skipping');
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (fitToContainer) {
|
|
1544
|
+
const padding = 40;
|
|
1545
|
+
const availableWidth = width - padding * 2;
|
|
1546
|
+
const availableHeight = height - padding * 2;
|
|
1547
|
+
const scaleX = availableWidth / (imageSize.width || 1);
|
|
1548
|
+
const scaleY = availableHeight / (imageSize.height || 1);
|
|
1549
|
+
const newScale = Math.min(scaleX, scaleY);
|
|
1550
|
+
const clampedScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
|
|
1551
|
+
this.scale.set(clampedScale);
|
|
1552
|
+
}
|
|
1553
|
+
// Skip if size hasn't changed to avoid unnecessary re-draws and potential loops (unless scale is being updated)
|
|
1554
|
+
// Use a small epsilon to avoid loops due to sub-pixel changes
|
|
1555
|
+
const sizeChanged = Math.abs(this.stage.width() - width) > 0.5 || Math.abs(this.stage.height() - height) > 0.5;
|
|
1556
|
+
const canvasSizeChanged = Math.abs(this.canvasRect.width() - imageSize.width) > 0.5 || Math.abs(this.canvasRect.height() - imageSize.height) > 0.5;
|
|
1557
|
+
const scaleChanged = Math.abs(this.canvasRect.scaleX() - this.scale()) > 0.001;
|
|
1558
|
+
if (!sizeChanged && !scaleChanged && !canvasSizeChanged) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (isNaN(width) || isNaN(height)) {
|
|
1562
|
+
console.warn('ImageDesignerService.updateSize: width or height is NaN', { width, height, container, imageSize });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
console.log('Updating stage size to:', width, 'x', height);
|
|
1566
|
+
this.stage.width(width);
|
|
1567
|
+
this.stage.height(height);
|
|
1568
|
+
const canvasWidth = imageSize.width || 0;
|
|
1569
|
+
const canvasHeight = imageSize.height || 0;
|
|
1570
|
+
console.log('Setting canvasRect size to:', canvasWidth, 'x', canvasHeight);
|
|
1571
|
+
this.canvasRect.width(canvasWidth);
|
|
1572
|
+
this.canvasRect.height(canvasHeight);
|
|
1573
|
+
this.canvasRect.offsetX(canvasWidth / 2);
|
|
1574
|
+
this.canvasRect.offsetY(canvasHeight / 2);
|
|
1575
|
+
const canvasX = width / 2;
|
|
1576
|
+
const canvasY = height / 2;
|
|
1577
|
+
if (!isNaN(canvasX) && !isNaN(canvasY)) {
|
|
1578
|
+
console.log('Centering canvasRect at:', canvasX, canvasY);
|
|
1579
|
+
this.canvasRect.x(canvasX);
|
|
1580
|
+
this.canvasRect.y(canvasY);
|
|
1581
|
+
this.canvasRect.scaleX(this.scale());
|
|
1582
|
+
this.canvasRect.scaleY(this.scale());
|
|
1583
|
+
// Update positions of all elements in workspaceLayer relative to new canvas position
|
|
1584
|
+
this.workspaceLayer.getChildren().forEach(child => {
|
|
1585
|
+
if (child !== this.canvasRect && child.name() !== 'transformer') {
|
|
1586
|
+
const originalX = child.attrs['originalX'] ?? 0;
|
|
1587
|
+
const originalY = child.attrs['originalY'] ?? 0;
|
|
1588
|
+
child.x(originalX * this.scale() + canvasX);
|
|
1589
|
+
child.y(originalY * this.scale() + canvasY);
|
|
1590
|
+
// Use the original scale for layers that have one (like flipped ones)
|
|
1591
|
+
const originalScaleX = child.attrs['originalScaleX'] ?? 1;
|
|
1592
|
+
const originalScaleY = child.attrs['originalScaleY'] ?? 1;
|
|
1593
|
+
child.scaleX(this.scale() * originalScaleX);
|
|
1594
|
+
child.scaleY(this.scale() * originalScaleY);
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
this.selectionRect?.visible(false); // Hide selection rect on resize as its coordinates are now invalid
|
|
1598
|
+
this.uiLayer.moveToTop(); // Ensure UI layer is always on top of workspace layer
|
|
1599
|
+
this.maskOverlay?.moveToTop(); // Mask on top of everything for clipping effect
|
|
1600
|
+
this.transformer?.moveToTop();
|
|
1601
|
+
this.hoverRect?.moveToTop();
|
|
1602
|
+
this.transformer?.forceUpdate();
|
|
1603
|
+
this.updateSelectionBorders();
|
|
1604
|
+
if (this.hoveredShape && this.hoverRect?.visible()) {
|
|
1605
|
+
this.showHover(this.hoveredShape);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
console.warn('ImageDesignerService.updateSize: Calculated canvas positions are NaN', { canvasX, canvasY, width, height, canvasWidth, scale: this.scale() });
|
|
1610
|
+
}
|
|
1611
|
+
this.uiLayer?.batchDraw();
|
|
1612
|
+
this.workspaceLayer?.batchDraw();
|
|
1613
|
+
if (notify) {
|
|
1614
|
+
this.notifyChange();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
setScale(scale) {
|
|
1618
|
+
const clampedScale = Math.min(Math.max(scale, this.minScale), this.maxScale);
|
|
1619
|
+
if (!this.isBrowser) {
|
|
1620
|
+
this.scale.set(clampedScale);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
this.scale.set(clampedScale);
|
|
1624
|
+
if (this.stage && this.canvasRect && this.workspaceLayer && this.uiLayer) {
|
|
1625
|
+
const container = this.stage.container();
|
|
1626
|
+
const imageSize = {
|
|
1627
|
+
width: this.canvasRect.width(),
|
|
1628
|
+
height: this.canvasRect.height()
|
|
1629
|
+
};
|
|
1630
|
+
// Update canvas positions and scales based on new scale
|
|
1631
|
+
this.updateSize(container, imageSize);
|
|
1632
|
+
this.transformer?.forceUpdate();
|
|
1633
|
+
this.uiLayer.batchDraw();
|
|
1634
|
+
this.workspaceLayer.batchDraw();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
zoomIn() {
|
|
1638
|
+
this.setScale(this.scale() + 0.1);
|
|
1639
|
+
}
|
|
1640
|
+
zoomOut() {
|
|
1641
|
+
this.setScale(this.scale() - 0.1);
|
|
1642
|
+
}
|
|
1643
|
+
setMinMaxScale(minScale, maxScale) {
|
|
1644
|
+
this.minScale = minScale;
|
|
1645
|
+
this.maxScale = maxScale;
|
|
1646
|
+
this.setScale(this.scale());
|
|
1647
|
+
}
|
|
1648
|
+
updateSnapSettings(settings) {
|
|
1649
|
+
this.snapSettings = { ...this.snapSettings, ...settings };
|
|
1650
|
+
}
|
|
1651
|
+
setCanvasBackground(config, notify = true) {
|
|
1652
|
+
if (!this.canvasRect)
|
|
1653
|
+
return;
|
|
1654
|
+
if (typeof config === 'string') {
|
|
1655
|
+
this.canvasRect.fill(config);
|
|
1656
|
+
this.canvasRect.fillLinearGradientStartPoint(null);
|
|
1657
|
+
this.canvasRect.fillLinearGradientEndPoint(null);
|
|
1658
|
+
this.canvasRect.fillLinearGradientColorStops([]);
|
|
1659
|
+
}
|
|
1660
|
+
else {
|
|
1661
|
+
this.canvasRect.fill(null);
|
|
1662
|
+
this.canvasRect.fillLinearGradientStartPoint({ x: config.x0 || 0, y: config.y0 || 0 });
|
|
1663
|
+
this.canvasRect.fillLinearGradientEndPoint({ x: config.x1 || 0, y: config.y1 || 0 });
|
|
1664
|
+
this.canvasRect.fillLinearGradientColorStops(config.colorStops);
|
|
1665
|
+
}
|
|
1666
|
+
this.workspaceLayer?.batchDraw();
|
|
1667
|
+
if (notify) {
|
|
1668
|
+
this.notifyChange();
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
reorderLayers(previousIndex, currentIndex) {
|
|
1672
|
+
if (!this.workspaceLayer || previousIndex === currentIndex)
|
|
1673
|
+
return;
|
|
1674
|
+
let updatedLayers = [];
|
|
1675
|
+
this._layers.update(layers => {
|
|
1676
|
+
const newLayers = [...layers];
|
|
1677
|
+
moveItemInArray(newLayers, previousIndex, currentIndex);
|
|
1678
|
+
// Update Konva nodes Z-index based on the new array order.
|
|
1679
|
+
// Array: [Top, ..., Bottom]
|
|
1680
|
+
// Konva: [Bottom, ..., Top]
|
|
1681
|
+
// So we need to reverse the array when applying to Konva Z-indices.
|
|
1682
|
+
const reversedLayers = [...newLayers].reverse();
|
|
1683
|
+
reversedLayers.forEach((layer, index) => {
|
|
1684
|
+
const shape = this.workspaceLayer?.findOne('#' + layer.id);
|
|
1685
|
+
if (shape) {
|
|
1686
|
+
// Layers start from zIndex 1 because canvasRect is at 0
|
|
1687
|
+
shape.zIndex(index + 1);
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
// Ensure canvasRect is at the bottom
|
|
1691
|
+
this.canvasRect?.zIndex(0);
|
|
1692
|
+
// Ensure transformer, selectionRect and hoverRect are always on top
|
|
1693
|
+
this.uiLayer?.moveToTop();
|
|
1694
|
+
this.maskOverlay?.moveToTop();
|
|
1695
|
+
this.transformer?.moveToTop();
|
|
1696
|
+
this.selectionRect?.moveToTop();
|
|
1697
|
+
this.hoverRect?.moveToTop();
|
|
1698
|
+
this.workspaceLayer?.batchDraw();
|
|
1699
|
+
this.uiLayer?.batchDraw();
|
|
1700
|
+
updatedLayers = newLayers;
|
|
1701
|
+
return newLayers;
|
|
1702
|
+
});
|
|
1703
|
+
if (updatedLayers.length > 0) {
|
|
1704
|
+
this.notifyChange();
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
currentSnapshotVersion = 0;
|
|
1708
|
+
getSnapshot() {
|
|
1709
|
+
return {
|
|
1710
|
+
version: this.currentSnapshotVersion,
|
|
1711
|
+
imageSize: {
|
|
1712
|
+
width: this.canvasRect?.width() || 0,
|
|
1713
|
+
height: this.canvasRect?.height() || 0
|
|
1714
|
+
},
|
|
1715
|
+
layers: this._layers().map(layer => {
|
|
1716
|
+
// Ensure that any temporary 'selected' property is not included in the snapshot
|
|
1717
|
+
const { selected, ...rest } = layer;
|
|
1718
|
+
return rest;
|
|
1719
|
+
}),
|
|
1720
|
+
background: this.canvasRect?.fill() || 'white',
|
|
1721
|
+
backgroundConfig: this.canvasRect?.fillLinearGradientColorStops()?.length ? {
|
|
1722
|
+
x0: this.canvasRect.fillLinearGradientStartPoint().x,
|
|
1723
|
+
y0: this.canvasRect.fillLinearGradientStartPoint().y,
|
|
1724
|
+
x1: this.canvasRect.fillLinearGradientEndPoint().x,
|
|
1725
|
+
y1: this.canvasRect.fillLinearGradientEndPoint().y,
|
|
1726
|
+
colorStops: this.canvasRect.fillLinearGradientColorStops()
|
|
1727
|
+
} : undefined
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
async loadSnapshot(snapshot, resetHistory = false) {
|
|
1731
|
+
if (!this.stage || !this.workspaceLayer || !this.canvasRect) {
|
|
1732
|
+
console.warn('loadSnapshot called but service is not fully initialized');
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (this.isSnapshotLoading) {
|
|
1736
|
+
console.log('loadSnapshot ignored - already loading');
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
this.isSnapshotLoading = true;
|
|
1740
|
+
try {
|
|
1741
|
+
console.log('loadSnapshot starting', snapshot);
|
|
1742
|
+
this.currentSnapshotVersion = snapshot.version ?? 0;
|
|
1743
|
+
// Clear current state
|
|
1744
|
+
this.selectedLayerId.set(null);
|
|
1745
|
+
this.transformer?.nodes([]);
|
|
1746
|
+
// Remove all shapes from workspace except canvasRect
|
|
1747
|
+
// Critical: iterate over a copy of the children array, as destroy() modifies it
|
|
1748
|
+
const children = [...this.workspaceLayer.getChildren()];
|
|
1749
|
+
children.forEach((child) => {
|
|
1750
|
+
if (child !== this.canvasRect) {
|
|
1751
|
+
child.destroy();
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
const { imageSize, layers, background, backgroundConfig } = snapshot;
|
|
1755
|
+
// Update size BEFORE adding layers so that addLayer uses correct canvas position and scale
|
|
1756
|
+
this.updateSize(this.stage.container(), imageSize);
|
|
1757
|
+
// Restore background
|
|
1758
|
+
if (backgroundConfig) {
|
|
1759
|
+
this.setCanvasBackground(backgroundConfig, false);
|
|
1760
|
+
}
|
|
1761
|
+
else if (background) {
|
|
1762
|
+
this.setCanvasBackground(background, false);
|
|
1763
|
+
}
|
|
1764
|
+
// Restore layers
|
|
1765
|
+
const reversedLayers = [...layers].reverse();
|
|
1766
|
+
const newLayers = [];
|
|
1767
|
+
for (const layerConfig of reversedLayers) {
|
|
1768
|
+
const id = layerConfig.id || Math.random().toString(36).substr(2, 9);
|
|
1769
|
+
const configWithId = { ...layerConfig, id };
|
|
1770
|
+
// We add layers to Konva but don't update the signal inside addLayer
|
|
1771
|
+
await this.addLayer(configWithId, false);
|
|
1772
|
+
// We collect them in the same order as in addLayer(..., true) which is [newConfig, ...layers]
|
|
1773
|
+
newLayers.unshift(configWithId);
|
|
1774
|
+
}
|
|
1775
|
+
this._layers.set(newLayers);
|
|
1776
|
+
this.workspaceLayer.batchDraw();
|
|
1777
|
+
this.uiLayer?.batchDraw();
|
|
1778
|
+
}
|
|
1779
|
+
finally {
|
|
1780
|
+
this.isSnapshotLoading = false;
|
|
1781
|
+
if (resetHistory) {
|
|
1782
|
+
this.resetHistory();
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
this.notifyChange(false);
|
|
1786
|
+
this.updateHistorySignals();
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
async loadAllFonts() {
|
|
1791
|
+
const fontPromises = [];
|
|
1792
|
+
// Always load default font
|
|
1793
|
+
fontPromises.push(this.loadFont(this.defaultFont));
|
|
1794
|
+
this._layers().forEach(layer => {
|
|
1795
|
+
if (layer.type === 'text' && layer['fontFamily']) {
|
|
1796
|
+
fontPromises.push(this.loadFont(layer['fontFamily']));
|
|
1797
|
+
}
|
|
1798
|
+
else if (layer.type === 'text') {
|
|
1799
|
+
fontPromises.push(this.loadFont(this.defaultFont));
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
await Promise.all(fontPromises);
|
|
1803
|
+
}
|
|
1804
|
+
loadFont(fontFamily) {
|
|
1805
|
+
if (!this.isBrowser || this.loadedFonts.has(fontFamily)) {
|
|
1806
|
+
return Promise.resolve();
|
|
1807
|
+
}
|
|
1808
|
+
return new Promise((resolve) => {
|
|
1809
|
+
const link = document.createElement('link');
|
|
1810
|
+
link.rel = 'stylesheet';
|
|
1811
|
+
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap`;
|
|
1812
|
+
link.onload = () => {
|
|
1813
|
+
this.loadedFonts.add(fontFamily);
|
|
1814
|
+
// Wait for font to be ready for rendering
|
|
1815
|
+
if ('fonts' in document) {
|
|
1816
|
+
document.fonts.load(`1em "${fontFamily}"`).then(() => {
|
|
1817
|
+
resolve();
|
|
1818
|
+
}).catch(() => {
|
|
1819
|
+
resolve();
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
// Fallback: small timeout
|
|
1824
|
+
setTimeout(() => resolve(), 100);
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
link.onerror = () => {
|
|
1828
|
+
console.error(`Failed to load font: ${fontFamily}`);
|
|
1829
|
+
resolve();
|
|
1830
|
+
};
|
|
1831
|
+
document.head.appendChild(link);
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
getLayerThumbnail(id, effectId) {
|
|
1835
|
+
if (!this.workspaceLayer)
|
|
1836
|
+
return null;
|
|
1837
|
+
const shape = this.workspaceLayer.findOne('#' + id);
|
|
1838
|
+
if (!shape)
|
|
1839
|
+
return null;
|
|
1840
|
+
try {
|
|
1841
|
+
if (effectId) {
|
|
1842
|
+
// Clone the shape to apply the effect without affecting the original
|
|
1843
|
+
const clone = shape.clone();
|
|
1844
|
+
const layer = this._layers().find(l => l.id === id);
|
|
1845
|
+
if (layer) {
|
|
1846
|
+
const tempConfig = { ...layer, effect: effectId };
|
|
1847
|
+
this.applyEffectsToShape(clone, tempConfig);
|
|
1848
|
+
}
|
|
1849
|
+
const dataUrl = clone.toDataURL({
|
|
1850
|
+
pixelRatio: 0.5,
|
|
1851
|
+
quality: 0.5
|
|
1852
|
+
});
|
|
1853
|
+
clone.destroy();
|
|
1854
|
+
return dataUrl;
|
|
1855
|
+
}
|
|
1856
|
+
// Create a small thumbnail of the shape
|
|
1857
|
+
return shape.toDataURL({
|
|
1858
|
+
pixelRatio: 0.5,
|
|
1859
|
+
quality: 0.5
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
catch (e) {
|
|
1863
|
+
console.warn('Failed to generate thumbnail for layer', id, e);
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
async updateLayer(id, config) {
|
|
1868
|
+
const layer = this._layers().find(l => l.id === id);
|
|
1869
|
+
if (!layer)
|
|
1870
|
+
return;
|
|
1871
|
+
const shape = this.workspaceLayer?.findOne('#' + id);
|
|
1872
|
+
if (config['scaleX'] !== undefined && shape) {
|
|
1873
|
+
shape.scaleX(config['scaleX']);
|
|
1874
|
+
}
|
|
1875
|
+
if (config['scaleY'] !== undefined && shape) {
|
|
1876
|
+
shape.scaleY(config['scaleY']);
|
|
1877
|
+
}
|
|
1878
|
+
if (config['x'] !== undefined && shape) {
|
|
1879
|
+
shape.x(config['x']);
|
|
1880
|
+
}
|
|
1881
|
+
if (config['y'] !== undefined && shape) {
|
|
1882
|
+
shape.y(config['y']);
|
|
1883
|
+
}
|
|
1884
|
+
if (config['rotation'] !== undefined && shape) {
|
|
1885
|
+
shape.rotation(config['rotation']);
|
|
1886
|
+
}
|
|
1887
|
+
if (config['width'] !== undefined && shape && !(shape instanceof Konva.Text)) {
|
|
1888
|
+
shape.width(config['width']);
|
|
1889
|
+
shape.offsetX(shape.width() / 2);
|
|
1890
|
+
}
|
|
1891
|
+
if (config['height'] !== undefined && shape && !(shape instanceof Konva.Text)) {
|
|
1892
|
+
shape.height(config['height']);
|
|
1893
|
+
shape.offsetY(shape.height() / 2);
|
|
1894
|
+
}
|
|
1895
|
+
if (config['width'] !== undefined && shape instanceof Konva.Text) {
|
|
1896
|
+
shape.width(config['width']);
|
|
1897
|
+
shape.offsetX(shape.width() / 2);
|
|
1898
|
+
}
|
|
1899
|
+
if (shape) {
|
|
1900
|
+
this.updateShapeOriginalPosition(shape, true);
|
|
1901
|
+
}
|
|
1902
|
+
this._layers.update(layers => layers.map(l => l.id === id ? { ...l, ...config } : l));
|
|
1903
|
+
const updatedLayer = this._layers().find(l => l.id === id);
|
|
1904
|
+
if (shape && updatedLayer) {
|
|
1905
|
+
this.applyEffectsToShape(shape, updatedLayer);
|
|
1906
|
+
}
|
|
1907
|
+
// If font-related properties changed, ensure the font variant is loaded and applied
|
|
1908
|
+
if (this.isBrowser && shape instanceof Konva.Text && (config['fontFamily'] !== undefined ||
|
|
1909
|
+
config['fontWeight'] !== undefined ||
|
|
1910
|
+
config['fontStyle'] !== undefined)) {
|
|
1911
|
+
const family = config['fontFamily'] || shape.fontFamily();
|
|
1912
|
+
const weight = config['fontWeight'] || shape.getAttr('fontWeight') || 400;
|
|
1913
|
+
const isItalic = (config['fontStyle'] === 'italic') || (shape.fontStyle() || '').includes('italic');
|
|
1914
|
+
const fontStr = `${isItalic ? 'italic ' : ''}${weight} 12px "${family}"`;
|
|
1915
|
+
if (config['fontFamily']) {
|
|
1916
|
+
await this.loadFont(config['fontFamily']);
|
|
1917
|
+
this.workspaceLayer?.batchDraw();
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
await document.fonts.load(fontStr);
|
|
1921
|
+
this.workspaceLayer?.batchDraw();
|
|
1922
|
+
}
|
|
1923
|
+
catch (e) {
|
|
1924
|
+
console.warn('Failed to load font variant:', fontStr);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (shape) {
|
|
1928
|
+
if (config.fill !== undefined) {
|
|
1929
|
+
const type = config.type || layer.type;
|
|
1930
|
+
if (type === 'shape' || type === 'pattern') {
|
|
1931
|
+
// For SVG shapes and patterns, we can replace the color in the SVG data.
|
|
1932
|
+
const currentLayer = this._layers().find(l => l.id === id);
|
|
1933
|
+
const dataKey = type === 'shape' ? 'data' : 'patternImage';
|
|
1934
|
+
const dataUrl = currentLayer?.[dataKey];
|
|
1935
|
+
if (typeof dataUrl === 'string' && dataUrl.startsWith('data:image/svg+xml') &&
|
|
1936
|
+
config.fill && !config.fill.startsWith('data:image') && !config.fill.startsWith('http')) {
|
|
1937
|
+
const commaIndex = dataUrl.indexOf(',');
|
|
1938
|
+
if (commaIndex !== -1) {
|
|
1939
|
+
const header = dataUrl.substring(0, commaIndex);
|
|
1940
|
+
const isBase64 = header.includes('base64');
|
|
1941
|
+
const svgText = isBase64
|
|
1942
|
+
? atob(dataUrl.substring(commaIndex + 1))
|
|
1943
|
+
: decodeURIComponent(dataUrl.substring(commaIndex + 1));
|
|
1944
|
+
const newFill = config.fill;
|
|
1945
|
+
// Replace both fill and stroke colors
|
|
1946
|
+
const newSvgText = svgText
|
|
1947
|
+
.replace(/fill="[#][0-9a-fA-F]{3,6}"/g, `fill="${newFill}"`)
|
|
1948
|
+
.replace(/fill='%23[0-9a-fA-F]{3,6}'/g, `fill='${newFill}'`)
|
|
1949
|
+
.replace(/stroke="[#][0-9a-fA-F]{3,6}"/g, `stroke="${newFill}"`)
|
|
1950
|
+
.replace(/stroke='%23[0-9a-fA-F]{3,6}'/g, `stroke='${newFill}'`);
|
|
1951
|
+
const newDataUrl = isBase64
|
|
1952
|
+
? `${header},${btoa(newSvgText)}`
|
|
1953
|
+
: `${header},${encodeURIComponent(newSvgText)}`;
|
|
1954
|
+
const img = new Image();
|
|
1955
|
+
img.crossOrigin = 'anonymous';
|
|
1956
|
+
img.onload = () => {
|
|
1957
|
+
if (type === 'shape') {
|
|
1958
|
+
shape.image(img);
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
shape.setAttr('fillPatternImage', img);
|
|
1962
|
+
}
|
|
1963
|
+
this.workspaceLayer?.batchDraw();
|
|
1964
|
+
};
|
|
1965
|
+
img.src = newDataUrl;
|
|
1966
|
+
// Update the data in the signal too
|
|
1967
|
+
this._layers.update(layers => layers.map(l => l.id === id ? { ...l, [dataKey]: newDataUrl } : l));
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
else if (config.fill && (config.fill.startsWith('data:image') || config.fill.startsWith('http'))) {
|
|
1971
|
+
const img = new Image();
|
|
1972
|
+
img.crossOrigin = 'anonymous';
|
|
1973
|
+
img.onload = () => {
|
|
1974
|
+
shape.setAttr('fillPatternImage', img);
|
|
1975
|
+
this.workspaceLayer?.batchDraw();
|
|
1976
|
+
};
|
|
1977
|
+
img.src = config.fill;
|
|
1978
|
+
if (type === 'pattern') {
|
|
1979
|
+
this._layers.update(layers => layers.map(l => l.id === id ? { ...l, patternImage: config.fill } : l));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
shape.setAttr('fill', config.fill);
|
|
1984
|
+
if (type !== 'pattern') {
|
|
1985
|
+
shape.setAttr('fillPatternImage', null);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
else if (config.fill && (config.fill.startsWith('data:image') || config.fill.startsWith('http'))) {
|
|
1990
|
+
// It's a pattern for other types
|
|
1991
|
+
const img = new Image();
|
|
1992
|
+
img.crossOrigin = 'anonymous';
|
|
1993
|
+
img.onload = () => {
|
|
1994
|
+
shape.setAttr('fillPatternImage', img);
|
|
1995
|
+
this.workspaceLayer?.batchDraw();
|
|
1996
|
+
};
|
|
1997
|
+
img.src = config.fill;
|
|
1998
|
+
}
|
|
1999
|
+
else {
|
|
2000
|
+
shape.setAttr('fill', config.fill);
|
|
2001
|
+
shape.setAttr('fillPatternImage', null);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (config['opacity'] !== undefined) {
|
|
2005
|
+
shape.opacity(config['opacity']);
|
|
2006
|
+
}
|
|
2007
|
+
if (config['text'] !== undefined && shape instanceof Konva.Text) {
|
|
2008
|
+
shape.text(config['text']);
|
|
2009
|
+
// Update offset because content changed
|
|
2010
|
+
shape.offsetX(shape.width() / 2);
|
|
2011
|
+
shape.offsetY(shape.height() / 2);
|
|
2012
|
+
}
|
|
2013
|
+
if (config['fontSize'] !== undefined && shape instanceof Konva.Text) {
|
|
2014
|
+
shape.fontSize(config['fontSize']);
|
|
2015
|
+
// Update offset because size changed
|
|
2016
|
+
shape.offsetX(shape.width() / 2);
|
|
2017
|
+
shape.offsetY(shape.height() / 2);
|
|
2018
|
+
}
|
|
2019
|
+
if (config['width'] !== undefined && shape instanceof Konva.Text) {
|
|
2020
|
+
shape.width(config['width'] * this.scale());
|
|
2021
|
+
// Update offset because width changed
|
|
2022
|
+
shape.offsetX(shape.width() / 2);
|
|
2023
|
+
shape.offsetY(shape.height() / 2);
|
|
2024
|
+
}
|
|
2025
|
+
if (config['fontFamily'] !== undefined && shape instanceof Konva.Text) {
|
|
2026
|
+
shape.fontFamily(config['fontFamily']);
|
|
2027
|
+
}
|
|
2028
|
+
if (config['fontWeight'] !== undefined && shape instanceof Konva.Text) {
|
|
2029
|
+
shape.setAttr('fontWeight', config['fontWeight']);
|
|
2030
|
+
// Konva's fontStyle property is used for weight and style.
|
|
2031
|
+
// If we have numeric fontWeight, we can try to use it directly in fontStyle as well.
|
|
2032
|
+
const currentStyle = (shape.fontStyle() || '').includes('italic') ? 'italic' : '';
|
|
2033
|
+
shape.fontStyle(`${config['fontWeight']} ${currentStyle}`.trim());
|
|
2034
|
+
// Update offset because size changed
|
|
2035
|
+
shape.offsetX(shape.width() / 2);
|
|
2036
|
+
shape.offsetY(shape.height() / 2);
|
|
2037
|
+
// Force text refresh to update layout before transformer update
|
|
2038
|
+
this.workspaceLayer?.batchDraw();
|
|
2039
|
+
}
|
|
2040
|
+
if (config['fontStyle'] !== undefined && shape instanceof Konva.Text) {
|
|
2041
|
+
// If fontStyle is provided, it might override fontWeight if we are not careful.
|
|
2042
|
+
// But usually fontStyle in this app means 'italic' or 'normal'.
|
|
2043
|
+
const currentWeight = shape.getAttr('fontWeight') || 400;
|
|
2044
|
+
if (config['fontStyle'] === 'italic') {
|
|
2045
|
+
shape.fontStyle(currentWeight + ' italic');
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
shape.fontStyle(currentWeight.toString());
|
|
2049
|
+
}
|
|
2050
|
+
// Update offset because size changed
|
|
2051
|
+
shape.offsetX(shape.width() / 2);
|
|
2052
|
+
shape.offsetY(shape.height() / 2);
|
|
2053
|
+
// Force text refresh to update layout before transformer update
|
|
2054
|
+
this.workspaceLayer?.batchDraw();
|
|
2055
|
+
}
|
|
2056
|
+
if (config['textDecoration'] !== undefined && shape instanceof Konva.Text) {
|
|
2057
|
+
shape.textDecoration(config['textDecoration']);
|
|
2058
|
+
}
|
|
2059
|
+
if (config['align'] !== undefined && shape instanceof Konva.Text) {
|
|
2060
|
+
shape.align(config['align']);
|
|
2061
|
+
}
|
|
2062
|
+
if (config['lineHeight'] !== undefined && shape instanceof Konva.Text) {
|
|
2063
|
+
shape.lineHeight(config['lineHeight']);
|
|
2064
|
+
}
|
|
2065
|
+
if (config['letterSpacing'] !== undefined && shape instanceof Konva.Text) {
|
|
2066
|
+
shape.letterSpacing(config['letterSpacing']);
|
|
2067
|
+
}
|
|
2068
|
+
if (config['textCase'] !== undefined && shape instanceof Konva.Text) {
|
|
2069
|
+
const text = layer.text || '';
|
|
2070
|
+
if (config['textCase'] === 'upper') {
|
|
2071
|
+
shape.text(text.toUpperCase());
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
shape.text(text);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
this.workspaceLayer?.batchDraw();
|
|
2078
|
+
this.uiLayer?.batchDraw();
|
|
2079
|
+
this.transformer?.forceUpdate();
|
|
2080
|
+
this.updateSelectionBorders();
|
|
2081
|
+
this.notifyChange();
|
|
2082
|
+
// If text properties changed, update again after a short delay
|
|
2083
|
+
// to handle cases where text layout hasn't updated yet.
|
|
2084
|
+
if (shape instanceof Konva.Text && (config['fontFamily'] !== undefined ||
|
|
2085
|
+
config['fontSize'] !== undefined ||
|
|
2086
|
+
config['text'] !== undefined ||
|
|
2087
|
+
config['fontWeight'] !== undefined ||
|
|
2088
|
+
config['fontStyle'] !== undefined)) {
|
|
2089
|
+
setTimeout(() => {
|
|
2090
|
+
if (shape instanceof Konva.Text) {
|
|
2091
|
+
// Re-calculate offsets one more time as dimensions might have changed
|
|
2092
|
+
// after the browser fully applied the font metrics.
|
|
2093
|
+
shape.offsetX(shape.width() / 2);
|
|
2094
|
+
shape.offsetY(shape.height() / 2);
|
|
2095
|
+
}
|
|
2096
|
+
if (this.transformer) {
|
|
2097
|
+
// Re-assigning nodes is the most reliable way to force Transformer
|
|
2098
|
+
// to re-calculate its boundaries and adapt to the new text size.
|
|
2099
|
+
const currentNodes = this.transformer.nodes();
|
|
2100
|
+
this.transformer.nodes([]);
|
|
2101
|
+
this.transformer.nodes(currentNodes);
|
|
2102
|
+
}
|
|
2103
|
+
this.workspaceLayer?.batchDraw();
|
|
2104
|
+
this.uiLayer?.batchDraw();
|
|
2105
|
+
this.transformer?.forceUpdate();
|
|
2106
|
+
this.updateSelectionBorders();
|
|
2107
|
+
}, 100);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
getBase64Image() {
|
|
2112
|
+
if (!this.stage || !this.canvasRect) {
|
|
2113
|
+
return undefined;
|
|
2114
|
+
}
|
|
2115
|
+
// Capture original state
|
|
2116
|
+
const wasUiLayerVisible = this.uiLayer?.visible() || false;
|
|
2117
|
+
const originalStroke = this.canvasRect.stroke();
|
|
2118
|
+
const originalStrokeWidth = this.canvasRect.strokeWidth();
|
|
2119
|
+
// Hide UI layer to exclude transformers, snap lines, etc. from export
|
|
2120
|
+
this.uiLayer?.visible(false);
|
|
2121
|
+
// Temporarily hide canvas border
|
|
2122
|
+
this.canvasRect.stroke(null);
|
|
2123
|
+
this.canvasRect.strokeWidth(0);
|
|
2124
|
+
// Get the client rect of the canvas area relative to the stage
|
|
2125
|
+
const rect = this.canvasRect.getClientRect({ relativeTo: this.stage });
|
|
2126
|
+
// Ensure we are in a browser
|
|
2127
|
+
if (!this.isBrowser) {
|
|
2128
|
+
return undefined;
|
|
2129
|
+
}
|
|
2130
|
+
try {
|
|
2131
|
+
const dataUrl = this.stage.toDataURL({
|
|
2132
|
+
x: rect.x,
|
|
2133
|
+
y: rect.y,
|
|
2134
|
+
width: rect.width,
|
|
2135
|
+
height: rect.height,
|
|
2136
|
+
pixelRatio: 2 // High quality
|
|
2137
|
+
});
|
|
2138
|
+
return dataUrl;
|
|
2139
|
+
}
|
|
2140
|
+
catch (e) {
|
|
2141
|
+
console.error('Failed to get base64 image:', e);
|
|
2142
|
+
return undefined;
|
|
2143
|
+
}
|
|
2144
|
+
finally {
|
|
2145
|
+
// Restore UI layer visibility
|
|
2146
|
+
if (wasUiLayerVisible) {
|
|
2147
|
+
this.uiLayer?.visible(true);
|
|
2148
|
+
}
|
|
2149
|
+
// Restore canvas border
|
|
2150
|
+
this.canvasRect.stroke(originalStroke);
|
|
2151
|
+
this.canvasRect.strokeWidth(originalStrokeWidth);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
downloadImage() {
|
|
2155
|
+
if (!this.stage || !this.canvasRect) {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
// Capture original state
|
|
2159
|
+
const wasUiLayerVisible = this.uiLayer?.visible() || false;
|
|
2160
|
+
const originalStroke = this.canvasRect.stroke();
|
|
2161
|
+
const originalStrokeWidth = this.canvasRect.strokeWidth();
|
|
2162
|
+
// Hide UI layer to exclude transformers, snap lines, etc. from export
|
|
2163
|
+
this.uiLayer?.visible(false);
|
|
2164
|
+
// Temporarily hide canvas border
|
|
2165
|
+
this.canvasRect.stroke(null);
|
|
2166
|
+
this.canvasRect.strokeWidth(0);
|
|
2167
|
+
// Get the client rect of the canvas area relative to the stage
|
|
2168
|
+
const rect = this.canvasRect.getClientRect({ relativeTo: this.stage });
|
|
2169
|
+
// Ensure we are in a browser
|
|
2170
|
+
if (!this.isBrowser) {
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
try {
|
|
2174
|
+
const dataUrl = this.stage.toDataURL({
|
|
2175
|
+
x: rect.x,
|
|
2176
|
+
y: rect.y,
|
|
2177
|
+
width: rect.width,
|
|
2178
|
+
height: rect.height,
|
|
2179
|
+
pixelRatio: 2 // High quality
|
|
2180
|
+
});
|
|
2181
|
+
const timestamp = new Date().getTime();
|
|
2182
|
+
const filename = `${timestamp}.png`;
|
|
2183
|
+
const downloadLink = document.createElement('a');
|
|
2184
|
+
downloadLink.href = dataUrl;
|
|
2185
|
+
downloadLink.download = filename;
|
|
2186
|
+
document.body.appendChild(downloadLink);
|
|
2187
|
+
downloadLink.click();
|
|
2188
|
+
document.body.removeChild(downloadLink);
|
|
2189
|
+
}
|
|
2190
|
+
catch (e) {
|
|
2191
|
+
console.error('Failed to export image:', e);
|
|
2192
|
+
}
|
|
2193
|
+
finally {
|
|
2194
|
+
// Restore UI layer visibility
|
|
2195
|
+
if (wasUiLayerVisible) {
|
|
2196
|
+
this.uiLayer?.visible(true);
|
|
2197
|
+
}
|
|
2198
|
+
// Restore canvas border
|
|
2199
|
+
this.canvasRect.stroke(originalStroke);
|
|
2200
|
+
this.canvasRect.strokeWidth(originalStrokeWidth);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
notifyChange(incrementVersion = true) {
|
|
2204
|
+
if (this.isSnapshotLoading)
|
|
2205
|
+
return;
|
|
2206
|
+
if (incrementVersion) {
|
|
2207
|
+
this.currentSnapshotVersion++;
|
|
2208
|
+
this.saveHistory();
|
|
2209
|
+
}
|
|
2210
|
+
this._change$.next();
|
|
2211
|
+
}
|
|
2212
|
+
saveHistory() {
|
|
2213
|
+
const snapshot = this.getSnapshot();
|
|
2214
|
+
// Don't save if it's the same as the last one
|
|
2215
|
+
if (this.undoStack.length > 0) {
|
|
2216
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
2217
|
+
if (JSON.stringify(last.layers) === JSON.stringify(snapshot.layers) &&
|
|
2218
|
+
JSON.stringify(last.imageSize) === JSON.stringify(snapshot.imageSize) &&
|
|
2219
|
+
last.background === snapshot.background &&
|
|
2220
|
+
JSON.stringify(last.backgroundConfig) === JSON.stringify(snapshot.backgroundConfig)) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
this.undoStack.push(snapshot);
|
|
2225
|
+
if (this.undoStack.length > this.historyLimit) {
|
|
2226
|
+
this.undoStack.shift();
|
|
2227
|
+
}
|
|
2228
|
+
this.redoStack = [];
|
|
2229
|
+
this.updateHistorySignals();
|
|
2230
|
+
}
|
|
2231
|
+
undo() {
|
|
2232
|
+
if (this.undoStack.length <= 1)
|
|
2233
|
+
return;
|
|
2234
|
+
const current = this.undoStack.pop();
|
|
2235
|
+
this.redoStack.push(current);
|
|
2236
|
+
const previous = this.undoStack[this.undoStack.length - 1];
|
|
2237
|
+
this.loadSnapshot(previous, false);
|
|
2238
|
+
}
|
|
2239
|
+
redo() {
|
|
2240
|
+
if (this.redoStack.length === 0)
|
|
2241
|
+
return;
|
|
2242
|
+
const next = this.redoStack.pop();
|
|
2243
|
+
this.undoStack.push(next);
|
|
2244
|
+
this.loadSnapshot(next, false);
|
|
2245
|
+
}
|
|
2246
|
+
updateHistorySignals() {
|
|
2247
|
+
this.canUndo.set(this.undoStack.length > 1);
|
|
2248
|
+
this.canRedo.set(this.redoStack.length > 0);
|
|
2249
|
+
}
|
|
2250
|
+
resetHistory() {
|
|
2251
|
+
this.undoStack = [this.getSnapshot()];
|
|
2252
|
+
this.redoStack = [];
|
|
2253
|
+
this.updateHistorySignals();
|
|
2254
|
+
}
|
|
2255
|
+
applyEffectsToShape(shape, config) {
|
|
2256
|
+
const filters = [];
|
|
2257
|
+
let tintR = 0;
|
|
2258
|
+
let tintG = 0;
|
|
2259
|
+
let tintB = 0;
|
|
2260
|
+
if (config['effect'] === 'grayscale') {
|
|
2261
|
+
filters.push(Konva.Filters.Grayscale);
|
|
2262
|
+
}
|
|
2263
|
+
else if (config['effect'] === 'sepia') {
|
|
2264
|
+
filters.push(Konva.Filters.Sepia);
|
|
2265
|
+
}
|
|
2266
|
+
else if (config['effect'] === 'invert') {
|
|
2267
|
+
filters.push(Konva.Filters.Invert);
|
|
2268
|
+
}
|
|
2269
|
+
else if (config['effect'] === 'cold') {
|
|
2270
|
+
tintR = -0.15;
|
|
2271
|
+
tintG = 0.05;
|
|
2272
|
+
tintB = 0.25;
|
|
2273
|
+
filters.push(TintFilter);
|
|
2274
|
+
}
|
|
2275
|
+
else if (config['effect'] === 'warm') {
|
|
2276
|
+
tintR = 0.25;
|
|
2277
|
+
tintG = 0.1;
|
|
2278
|
+
tintB = -0.15;
|
|
2279
|
+
filters.push(TintFilter);
|
|
2280
|
+
}
|
|
2281
|
+
if (config['temperatureEnabled']) {
|
|
2282
|
+
const temp = (config['temperature'] ?? 0) / 100; // -1 to 1
|
|
2283
|
+
if (!filters.includes(TintFilter))
|
|
2284
|
+
filters.push(TintFilter);
|
|
2285
|
+
if (temp > 0) {
|
|
2286
|
+
tintR += (1 - tintR) * temp * 0.2;
|
|
2287
|
+
tintG += (1 - tintG) * temp * 0.1;
|
|
2288
|
+
tintB -= (1 + tintB) * temp * 0.2;
|
|
2289
|
+
}
|
|
2290
|
+
else {
|
|
2291
|
+
const absTemp = Math.abs(temp);
|
|
2292
|
+
tintR -= (1 + tintR) * absTemp * 0.2;
|
|
2293
|
+
tintG += (1 - tintG) * absTemp * 0.1;
|
|
2294
|
+
tintB += (1 - tintB) * absTemp * 0.2;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
shape.setAttr('tintR', tintR);
|
|
2298
|
+
shape.setAttr('tintG', tintG);
|
|
2299
|
+
shape.setAttr('tintB', tintB);
|
|
2300
|
+
if (config['blurEnabled']) {
|
|
2301
|
+
filters.push(Konva.Filters.Blur);
|
|
2302
|
+
shape.setAttr('blurRadius', config['blur'] ?? 0);
|
|
2303
|
+
}
|
|
2304
|
+
if (config['brightnessEnabled']) {
|
|
2305
|
+
filters.push(Konva.Filters.Brighten);
|
|
2306
|
+
shape.setAttr('brightness', config['brightness'] ?? 0);
|
|
2307
|
+
}
|
|
2308
|
+
if (config['contrastEnabled']) {
|
|
2309
|
+
filters.push(Konva.Filters.Contrast);
|
|
2310
|
+
shape.setAttr('contrast', config['contrast'] ?? 0);
|
|
2311
|
+
}
|
|
2312
|
+
if (config['saturationEnabled']) {
|
|
2313
|
+
filters.push(Konva.Filters.HSL);
|
|
2314
|
+
shape.setAttr('saturation', config['saturation'] ?? 0);
|
|
2315
|
+
}
|
|
2316
|
+
if (config['vibranceEnabled']) {
|
|
2317
|
+
// Konva doesn't have Vibrance by default, using HSL saturation as fallback
|
|
2318
|
+
if (!filters.includes(Konva.Filters.HSL)) {
|
|
2319
|
+
filters.push(Konva.Filters.HSL);
|
|
2320
|
+
}
|
|
2321
|
+
shape.setAttr('saturation', (config['saturation'] ?? 0) + (config['vibrance'] ?? 0) / 2);
|
|
2322
|
+
}
|
|
2323
|
+
if (config['borderEnabled']) {
|
|
2324
|
+
shape.setAttr('stroke', config['borderColor'] || config['fill'] || '#000000');
|
|
2325
|
+
shape.setAttr('strokeWidth', config['border'] ?? 0);
|
|
2326
|
+
}
|
|
2327
|
+
else {
|
|
2328
|
+
shape.setAttr('strokeWidth', 0);
|
|
2329
|
+
}
|
|
2330
|
+
if (config['cornerRadiusEnabled']) {
|
|
2331
|
+
const radius = config['cornerRadius'] ?? 0;
|
|
2332
|
+
if (shape instanceof Konva.Rect || shape instanceof Konva.Image) {
|
|
2333
|
+
shape.cornerRadius(radius);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
else {
|
|
2337
|
+
if (shape instanceof Konva.Rect || shape instanceof Konva.Image) {
|
|
2338
|
+
shape.cornerRadius(0);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
if (config['shadowEnabled']) {
|
|
2342
|
+
shape.setAttr('shadowColor', config['shadowColor'] || '#000000');
|
|
2343
|
+
shape.setAttr('shadowBlur', config['shadowBlur'] ?? 15);
|
|
2344
|
+
shape.setAttr('shadowOpacity', (config['shadowOpacity'] ?? 100) / 100);
|
|
2345
|
+
shape.setAttr('shadowOffset', {
|
|
2346
|
+
x: config['shadowOffsetX'] ?? 0,
|
|
2347
|
+
y: config['shadowOffsetY'] ?? 0
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
shape.setAttr('shadowOpacity', 0);
|
|
2352
|
+
}
|
|
2353
|
+
shape.filters(filters);
|
|
2354
|
+
if (filters.length > 0 || (config['cornerRadiusEnabled'] && config['cornerRadius'] > 0)) {
|
|
2355
|
+
// In Konva, caching is required for filters to work, and for cornerRadius to clip Image
|
|
2356
|
+
shape.clearCache();
|
|
2357
|
+
shape.cache();
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
shape.clearCache();
|
|
2361
|
+
}
|
|
2362
|
+
this.workspaceLayer?.batchDraw();
|
|
2363
|
+
}
|
|
2364
|
+
destroy() {
|
|
2365
|
+
this.isInitialized.set(false);
|
|
2366
|
+
if (this.isBrowser) {
|
|
2367
|
+
window.removeEventListener('mouseup', this.handleGlobalMouseUp);
|
|
2368
|
+
window.removeEventListener('touchend', this.handleGlobalMouseUp);
|
|
2369
|
+
window.removeEventListener('mousemove', this.handleGlobalMouseMove);
|
|
2370
|
+
window.removeEventListener('touchmove', this.handleGlobalMouseMove);
|
|
2371
|
+
}
|
|
2372
|
+
this.resizeObserver?.disconnect();
|
|
2373
|
+
this.stage?.off('mousedown touchstart');
|
|
2374
|
+
this.stage?.off('mousemove touchmove');
|
|
2375
|
+
this.stage?.off('mouseup touchend');
|
|
2376
|
+
this.hideHover();
|
|
2377
|
+
this.stage?.destroy();
|
|
2378
|
+
this.stage = undefined;
|
|
2379
|
+
this.uiLayer = undefined;
|
|
2380
|
+
this.workspaceLayer = undefined;
|
|
2381
|
+
this.canvasRect = undefined;
|
|
2382
|
+
this.maskOverlay = undefined;
|
|
2383
|
+
this.transformer = undefined;
|
|
2384
|
+
this.selectionRect = undefined;
|
|
2385
|
+
this.hoverRect = undefined;
|
|
2386
|
+
this.selectionBordersGroup = undefined;
|
|
2387
|
+
}
|
|
2388
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2389
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, providedIn: 'root' });
|
|
2390
|
+
}
|
|
2391
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, decorators: [{
|
|
2392
|
+
type: Injectable,
|
|
2393
|
+
args: [{
|
|
2394
|
+
providedIn: 'root'
|
|
2395
|
+
}]
|
|
2396
|
+
}] });
|
|
2397
|
+
|
|
2398
|
+
function createDefaultPhotosDataSource(http) {
|
|
2399
|
+
return {
|
|
2400
|
+
getItems: (params) => {
|
|
2401
|
+
const url = `https://picsum.photos/v2/list?page=${params.page}&limit=${params.pageSize}`;
|
|
2402
|
+
http.get(url).pipe(map(images => images.map(img => ({
|
|
2403
|
+
id: img.id,
|
|
2404
|
+
name: img.author,
|
|
2405
|
+
width: img.width,
|
|
2406
|
+
height: img.height,
|
|
2407
|
+
url: img.download_url,
|
|
2408
|
+
thumbUrl: `https://picsum.photos/id/${img.id}/${img.width > img.height ? 400 : Math.round(400 * (img.width / img.height))}/${img.height > img.width ? 400 : Math.round(400 * (img.height / img.width))}`
|
|
2409
|
+
})))).subscribe({
|
|
2410
|
+
next: (data) => params.successCallback(data),
|
|
2411
|
+
error: () => params.failCallback()
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const SVG_PATTERNS = [
|
|
2418
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="2" fill="#64748b"/></svg>`,
|
|
2419
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="3" fill="#64748b"/></svg>`,
|
|
2420
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="4" height="4" x="10" y="10" fill="#64748b"/></svg>`,
|
|
2421
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="6" height="6" x="20" y="20" fill="#64748b"/></svg>`,
|
|
2422
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="0" x2="40" y2="40" stroke="#64748b"/></svg>`,
|
|
2423
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="40" y1="0" x2="0" y2="40" stroke="#64748b"/></svg>`,
|
|
2424
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="10" stroke="#64748b" fill="none"/></svg>`,
|
|
2425
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="20" height="20" x="10" y="10" stroke="#64748b" fill="none"/></svg>`,
|
|
2426
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 H40" stroke="#64748b"/></svg>`,
|
|
2427
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M20 0 V40" stroke="#64748b"/></svg>`,
|
|
2428
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="5" r="2" fill="#64748b"/><circle cx="25" cy="25" r="2" fill="#64748b"/></svg>`,
|
|
2429
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="0" width="8" height="8" fill="#64748b"/></svg>`,
|
|
2430
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="32" y="32" width="8" height="8" fill="#64748b"/></svg>`,
|
|
2431
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="30" r="3" fill="#64748b"/></svg>`,
|
|
2432
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="10" r="3" fill="#64748b"/></svg>`,
|
|
2433
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 10 H40" stroke="#64748b"/><path d="M0 30 H40" stroke="#64748b"/></svg>`,
|
|
2434
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="20" cy="30" r="2" fill="#64748b"/></svg>`,
|
|
2435
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="10" y="10" width="5" height="5" fill="#64748b"/><rect x="25" y="25" width="5" height="5" fill="#64748b"/></svg>`,
|
|
2436
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="5" fill="#64748b"/></svg>`,
|
|
2437
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,5 25,15 15,15" fill="#64748b"/></svg>`,
|
|
2438
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,35 25,25 15,25" fill="#64748b"/></svg>`,
|
|
2439
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,20 15,15 15,25" fill="#64748b"/></svg>`,
|
|
2440
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="35,20 25,15 25,25" fill="#64748b"/></svg>`,
|
|
2441
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/></svg>`,
|
|
2442
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="20" x2="40" y2="20" stroke="#64748b"/></svg>`,
|
|
2443
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="20" y1="0" x2="20" y2="40" stroke="#64748b"/></svg>`,
|
|
2444
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="0" x2="20" y2="20" stroke="#64748b"/></svg>`,
|
|
2445
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="40" y1="0" x2="20" y2="20" stroke="#64748b"/></svg>`,
|
|
2446
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="40" x2="20" y2="20" stroke="#64748b"/></svg>`,
|
|
2447
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="2" fill="#64748b"/><circle cx="30" cy="20" r="2" fill="#64748b"/></svg>`,
|
|
2448
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="20" cy="30" r="2" fill="#64748b"/></svg>`,
|
|
2449
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="15" y="15" width="10" height="10" fill="#64748b"/></svg>`,
|
|
2450
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="5" r="1" fill="#64748b"/><circle cx="35" cy="35" r="1" fill="#64748b"/></svg>`,
|
|
2451
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="35" cy="5" r="1" fill="#64748b"/><circle cx="5" cy="35" r="1" fill="#64748b"/></svg>`,
|
|
2452
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M5 5 L35 35" stroke="#64748b"/></svg>`,
|
|
2453
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M35 5 L5 35" stroke="#64748b"/></svg>`,
|
|
2454
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="1" fill="#64748b"/></svg>`,
|
|
2455
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="18" y="18" width="4" height="4" fill="#64748b"/></svg>`,
|
|
2456
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,10 30,30 10,30" fill="none" stroke="#64748b"/></svg>`,
|
|
2457
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="1" fill="#64748b"/><circle cx="30" cy="30" r="1" fill="#64748b"/></svg>`,
|
|
2458
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="10" r="1" fill="#64748b"/><circle cx="10" cy="30" r="1" fill="#64748b"/></svg>`,
|
|
2459
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M10 10 H30 V30 H10 Z" fill="none" stroke="#64748b"/></svg>`,
|
|
2460
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="5" r="2" fill="#64748b"/><circle cx="20" cy="35" r="2" fill="#64748b"/></svg>`,
|
|
2461
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/></svg>`,
|
|
2462
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="18" width="40" height="4" fill="#64748b"/></svg>`,
|
|
2463
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="18" y="0" width="4" height="40" fill="#64748b"/></svg>`,
|
|
2464
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="4" fill="none" stroke="#64748b"/></svg>`,
|
|
2465
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="20" r="4" fill="none" stroke="#64748b"/></svg>`,
|
|
2466
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="8" fill="none" stroke="#64748b"/></svg>`,
|
|
2467
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 L20 0 L40 20 L20 40 Z" fill="none" stroke="#64748b"/></svg>`,
|
|
2468
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 10 L10 0 L20 10 L30 0 L40 10" stroke="#64748b" fill="none"/></svg>`,
|
|
2469
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 30 L10 40 L20 30 L30 40 L40 30" stroke="#64748b" fill="none"/></svg>`,
|
|
2470
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="2" fill="#64748b"/><circle cx="10" cy="10" r="2" fill="#64748b"/><circle cx="30" cy="30" r="2" fill="#64748b"/></svg>`,
|
|
2471
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="0" width="20" height="20" fill="none" stroke="#64748b"/><rect x="20" y="20" width="20" height="20" fill="none" stroke="#64748b"/></svg>`,
|
|
2472
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="20" y="0" width="20" height="20" fill="none" stroke="#64748b"/><rect x="0" y="20" width="20" height="20" fill="none" stroke="#64748b"/></svg>`,
|
|
2473
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 0 L40 20 L0 40 Z" fill="none" stroke="#64748b"/></svg>`,
|
|
2474
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M40 0 L0 20 L40 40 Z" fill="none" stroke="#64748b"/></svg>`,
|
|
2475
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="10,0 30,0 40,20 30,40 10,40 0,20" fill="none" stroke="#64748b"/></svg>`,
|
|
2476
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 0 L20 20 L40 0" stroke="#64748b" fill="none"/></svg>`,
|
|
2477
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 40 L20 20 L40 40" stroke="#64748b" fill="none"/></svg>`,
|
|
2478
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="0" cy="0" r="5" fill="#64748b"/><circle cx="40" cy="40" r="5" fill="#64748b"/></svg>`,
|
|
2479
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="40" cy="0" r="5" fill="#64748b"/><circle cx="0" cy="40" r="5" fill="#64748b"/></svg>`,
|
|
2480
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,5 35,35 5,35" fill="none" stroke="#64748b"/></svg>`,
|
|
2481
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,5 35,5 20,35" fill="none" stroke="#64748b"/></svg>`,
|
|
2482
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="5" y="5" width="30" height="30" rx="5" fill="none" stroke="#64748b"/></svg>`,
|
|
2483
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="10" y="10" width="20" height="20" rx="10" fill="none" stroke="#64748b"/></svg>`,
|
|
2484
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="12" stroke="#64748b" fill="none"/></svg>`,
|
|
2485
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="4" fill="#64748b"/><circle cx="30" cy="20" r="4" fill="#64748b"/></svg>`,
|
|
2486
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="4" fill="#64748b"/><circle cx="20" cy="30" r="4" fill="#64748b"/></svg>`,
|
|
2487
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 Q10 0 20 20 T40 20" stroke="#64748b" fill="none"/></svg>`,
|
|
2488
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 Q10 40 20 20 T40 20" stroke="#64748b" fill="none"/></svg>`,
|
|
2489
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="15" y="0" width="10" height="40" fill="#64748b"/></svg>`,
|
|
2490
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="15" width="40" height="10" fill="#64748b"/></svg>`,
|
|
2491
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,20 10,10 20,20 10,30" fill="none" stroke="#64748b"/></svg>`,
|
|
2492
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,0 30,10 20,20 10,10" fill="none" stroke="#64748b"/></svg>`,
|
|
2493
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="20" cy="5" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/><circle cx="20" cy="35" r="2" fill="#64748b"/></svg>`,
|
|
2494
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M10 0 L20 10 L30 0 L40 10" stroke="#64748b" fill="none"/></svg>`,
|
|
2495
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 30 L10 20 L20 30 L30 20 L40 30" stroke="#64748b" fill="none"/></svg>`,
|
|
2496
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,0 40,0 20,20" fill="none" stroke="#64748b"/></svg>`,
|
|
2497
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,40 40,40 20,20" fill="none" stroke="#64748b"/></svg>`,
|
|
2498
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="3" fill="#64748b"/><circle cx="30" cy="10" r="3" fill="#64748b"/><circle cx="20" cy="30" r="3" fill="#64748b"/></svg>`,
|
|
2499
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="10" cy="30" r="2" fill="#64748b"/><circle cx="30" cy="30" r="2" fill="#64748b"/></svg>`,
|
|
2500
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="5" y="5" width="10" height="10" fill="#64748b"/><rect x="25" y="25" width="10" height="10" fill="#64748b"/></svg>`,
|
|
2501
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="25" y="5" width="10" height="10" fill="#64748b"/><rect x="5" y="25" width="10" height="10" fill="#64748b"/></svg>`,
|
|
2502
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="3" fill="#64748b"/><circle cx="20" cy="5" r="3" fill="#64748b"/><circle cx="5" cy="20" r="3" fill="#64748b"/><circle cx="35" cy="20" r="3" fill="#64748b"/></svg>`,
|
|
2503
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="10,20 20,10 30,20 20,30" fill="none" stroke="#64748b"/></svg>`,
|
|
2504
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="6" fill="none" stroke="#64748b"/><circle cx="20" cy="20" r="2" fill="#64748b"/></svg>`,
|
|
2505
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,20 20,5 35,20 20,35" fill="none" stroke="#64748b"/></svg>`,
|
|
2506
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="14" fill="none" stroke="#64748b"/></svg>`,
|
|
2507
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,10 40,10 20,30" fill="none" stroke="#64748b"/></svg>`
|
|
2508
|
+
];
|
|
2509
|
+
|
|
2510
|
+
const SVG_ELEMENTS = [
|
|
2511
|
+
{
|
|
2512
|
+
name: 'square-filled',
|
|
2513
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="15" y="15" width="70" height="70" fill="%2364748b"/></svg>'
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
name: 'square-outline',
|
|
2517
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="15" y="15" width="70" height="70" fill="none" stroke="%2364748b" stroke-width="8"/></svg>'
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
name: 'rectangle-filled',
|
|
2521
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="25" width="80" height="50" fill="%2364748b"/></svg>'
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
name: 'rectangle-outline',
|
|
2525
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="25" width="80" height="50" fill="none" stroke="%2364748b" stroke-width="8"/></svg>'
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
name: 'arrow-right-1',
|
|
2529
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 45H65L50 30L90 50L50 70L65 55H10Z"/></svg>'
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
name: 'arrow-right-2',
|
|
2533
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 60,40 60,25 90,50 60,75 60,60 10,60"/></svg>'
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
name: 'arrow-right-3',
|
|
2537
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,45 55,45 55,30 90,50 55,70 55,55 10,55"/></svg>'
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
name: 'arrow-right-4',
|
|
2541
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="15,40 65,40 65,25 90,50 65,75 65,60 15,60"/></svg>'
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
name: 'arrow-right-5',
|
|
2545
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,42 58,42 58,28 90,50 58,72 58,58 10,58"/></svg>'
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
name: 'arrow-right-fat',
|
|
2549
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,35 55,35 55,20 90,50 55,80 55,65 10,65"/></svg>'
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
name: 'arrow-right-wide',
|
|
2553
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,38 60,38 60,22 90,50 60,78 60,62 10,62"/></svg>'
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
name: 'arrow-right-long',
|
|
2557
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="5,45 70,45 70,30 95,50 70,70 70,55 5,55"/></svg>'
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
name: 'arrow-right-compact',
|
|
2561
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,45 60,45 60,35 85,50 60,65 60,55 20,55"/></svg>'
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
name: 'arrow-right-block',
|
|
2565
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 55,40 55,25 85,50 55,75 55,60 10,60"/></svg>'
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
name: 'arrow-right',
|
|
2569
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M20 45H65L50 30L80 50L50 70L65 55H20Z"/></svg>'
|
|
2570
|
+
},
|
|
2571
|
+
{
|
|
2572
|
+
name: 'arrow-left',
|
|
2573
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M80 45H35L50 30L20 50L50 70L35 55H80Z"/></svg>'
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
name: 'arrow-up',
|
|
2577
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M45 80V35L30 50L50 20L70 50L55 35V80Z"/></svg>'
|
|
2578
|
+
},
|
|
2579
|
+
{
|
|
2580
|
+
name: 'triangle',
|
|
2581
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,90 10,90"/></svg>'
|
|
2582
|
+
},
|
|
2583
|
+
{
|
|
2584
|
+
name: 'diamond',
|
|
2585
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,50 50,90 10,50"/></svg>'
|
|
2586
|
+
},
|
|
2587
|
+
{
|
|
2588
|
+
name: 'pentagon',
|
|
2589
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,40 70,90 30,90 10,40"/></svg>'
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
name: 'parallelogram',
|
|
2593
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,20 90,20 70,80 0,80"/></svg>'
|
|
2594
|
+
},
|
|
2595
|
+
{
|
|
2596
|
+
name: 'star',
|
|
2597
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 61,40 95,40 67,60 78,90 50,70 22,90 33,60 5,40 39,40"/></svg>'
|
|
2598
|
+
},
|
|
2599
|
+
{
|
|
2600
|
+
name: 'hexagon',
|
|
2601
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="30,10 70,10 90,50 70,90 30,90 10,50"/></svg>'
|
|
2602
|
+
},
|
|
2603
|
+
{
|
|
2604
|
+
name: 'chevron',
|
|
2605
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,30 50,60 80,30 80,50 50,80 20,50"/></svg>'
|
|
2606
|
+
},
|
|
2607
|
+
{
|
|
2608
|
+
name: 'arrow',
|
|
2609
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 60,10 60,35 90,35 90,65 60,65 60,90"/></svg>'
|
|
2610
|
+
},
|
|
2611
|
+
{
|
|
2612
|
+
name: 'cross',
|
|
2613
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="40,10 60,10 60,40 90,40 90,60 60,60 60,90 40,90 40,60 10,60 10,40 40,40"/></svg>'
|
|
2614
|
+
},
|
|
2615
|
+
{
|
|
2616
|
+
name: 'trapezoid',
|
|
2617
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,30 80,30 65,80 35,80"/></svg>'
|
|
2618
|
+
},
|
|
2619
|
+
{
|
|
2620
|
+
name: 'kite',
|
|
2621
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 80,50 50,90 20,50"/></svg>'
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
name: 'zigzag',
|
|
2625
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 40,30 25,55 60,55 45,80 90,80 90,90 10,90"/></svg>'
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
name: 'notched-rect',
|
|
2629
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 70,20 90,40 90,80 10,80"/></svg>'
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
name: 'double-triangle',
|
|
2633
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,80 40,20 60,80"/><polygon fill="%2364748b" points="50,80 70,20 90,80"/></svg>'
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
name: 'shield',
|
|
2637
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 10 L85 25 L80 70 L50 90 L20 70 L15 25 Z"/></svg>'
|
|
2638
|
+
},
|
|
2639
|
+
{
|
|
2640
|
+
name: 'octagon',
|
|
2641
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="30,10 70,10 90,30 90,70 70,90 30,90 10,70 10,30"/></svg>'
|
|
2642
|
+
},
|
|
2643
|
+
{
|
|
2644
|
+
name: 'right-triangle',
|
|
2645
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,90 10,10 90,90"/></svg>'
|
|
2646
|
+
},
|
|
2647
|
+
{
|
|
2648
|
+
name: 'corner-cut',
|
|
2649
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,10 80,10 90,20 90,80 80,90 20,90 10,80 10,20"/></svg>'
|
|
2650
|
+
},
|
|
2651
|
+
{
|
|
2652
|
+
name: 'arrow-wide',
|
|
2653
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 50,10 50,30 90,30 90,70 50,70 50,90"/></svg>'
|
|
2654
|
+
},
|
|
2655
|
+
{
|
|
2656
|
+
name: 'triangle-down',
|
|
2657
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 90,20 50,90"/></svg>'
|
|
2658
|
+
},
|
|
2659
|
+
{
|
|
2660
|
+
name: 'triangle-left',
|
|
2661
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="80,10 20,50 80,90"/></svg>'
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
name: 'triangle-right',
|
|
2665
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,10 80,50 20,90"/></svg>'
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
name: 'wide-trapezoid',
|
|
2669
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 90,30 70,80 30,80"/></svg>'
|
|
2670
|
+
},
|
|
2671
|
+
{
|
|
2672
|
+
name: 'cut-diamond',
|
|
2673
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,5 90,50 50,95 10,50 30,50"/></svg>'
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
name: 'house',
|
|
2677
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 50,10 90,50 90,90 10,90"/></svg>'
|
|
2678
|
+
},
|
|
2679
|
+
{
|
|
2680
|
+
name: 'tag',
|
|
2681
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 50,10 90,40 70,90 30,90"/></svg>'
|
|
2682
|
+
},
|
|
2683
|
+
{
|
|
2684
|
+
name: 'double-chevron',
|
|
2685
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 40,50 10,70 25,70 55,50 25,30"/><polygon fill="%2364748b" points="45,30 75,50 45,70 60,70 90,50 60,30"/></svg>'
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
name: 'diamond-wide',
|
|
2689
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 95,50 50,90 5,50"/></svg>'
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: 'notch-top',
|
|
2693
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 40,20 50,10 60,20 90,20 90,90 10,90"/></svg>'
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
name: 'notch-bottom',
|
|
2697
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,10 90,10 90,80 60,80 50,90 40,80 10,80"/></svg>'
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
name: 'step-shape',
|
|
2701
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,80 10,40 40,40 40,20 90,20 90,80"/></svg>'
|
|
2702
|
+
},
|
|
2703
|
+
{
|
|
2704
|
+
name: 'skew-arrow',
|
|
2705
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,60 60,20 60,40 90,40 90,80 60,80 60,90"/></svg>'
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
name: 'barbed',
|
|
2709
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 30,30 50,50 70,30 90,50 70,70 50,50 30,70"/></svg>'
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
name: 'circle',
|
|
2713
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="40"/></svg>'
|
|
2714
|
+
},
|
|
2715
|
+
{
|
|
2716
|
+
name: 'circle-ring',
|
|
2717
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="40"/><circle fill="white" cx="50" cy="50" r="20"/></svg>'
|
|
2718
|
+
},
|
|
2719
|
+
{
|
|
2720
|
+
name: 'circle-cut-top',
|
|
2721
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="60" r="35"/></svg>'
|
|
2722
|
+
},
|
|
2723
|
+
{
|
|
2724
|
+
name: 'circle-half',
|
|
2725
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 50 A40 40 0 0 1 90 50 L90 90 L10 90 Z"/></svg>'
|
|
2726
|
+
},
|
|
2727
|
+
{
|
|
2728
|
+
name: 'circle-quarter',
|
|
2729
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 50 L90 50 A40 40 0 0 0 50 10 Z"/></svg>'
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
name: 'circle-pacman',
|
|
2733
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 10 A40 40 0 1 1 50 90 L70 50 Z"/></svg>'
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
name: 'circle-segment',
|
|
2737
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 50 L50 10 A40 40 0 0 1 90 50 Z"/></svg>'
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
name: 'circle-dot',
|
|
2741
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="35"/><circle fill="white" cx="50" cy="50" r="10"/></svg>'
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
name: 'circle-double',
|
|
2745
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="40" cy="50" r="25"/><circle fill="%2364748b" cx="65" cy="50" r="25"/></svg>'
|
|
2746
|
+
},
|
|
2747
|
+
{
|
|
2748
|
+
name: 'circle-arc',
|
|
2749
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 50 A40 40 0 0 1 90 50 L70 50 A20 20 0 0 0 30 50 Z"/></svg>'
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: 'blob-1',
|
|
2753
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M32 15C50 5 78 18 85 40C92 65 70 90 45 85C20 80 10 55 15 35C20 25 25 18 32 15Z"/></svg>'
|
|
2754
|
+
},
|
|
2755
|
+
{
|
|
2756
|
+
name: 'blob-2',
|
|
2757
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M40 10C65 5 90 25 85 50C80 80 55 95 35 85C15 75 10 50 20 30C25 20 30 15 40 10Z"/></svg>'
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
name: 'blob-3',
|
|
2761
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M28 20C45 5 75 10 85 35C95 60 75 90 50 88C25 85 10 60 15 40C18 30 22 25 28 20Z"/></svg>'
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
name: 'blob-4',
|
|
2765
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M35 12C55 2 85 18 88 42C92 70 70 92 45 88C20 85 8 60 15 38C18 28 25 18 35 12Z"/></svg>'
|
|
2766
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
name: 'blob-5',
|
|
2769
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M30 18C52 4 80 15 88 38C95 62 75 90 50 90C25 90 10 65 15 42C18 30 22 25 30 18Z"/></svg>'
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
name: 'blob-6',
|
|
2773
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M38 14C60 5 85 20 90 45C95 70 70 92 45 88C20 85 5 60 12 38C18 25 25 18 38 14Z"/></svg>'
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
name: 'blob-7',
|
|
2777
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M25 22C45 8 78 12 88 35C98 60 78 92 50 90C22 88 8 60 12 40C15 32 18 26 25 22Z"/></svg>'
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
name: 'blob-8',
|
|
2781
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M35 18C58 6 85 18 92 40C98 65 78 90 52 88C25 85 10 60 15 38C18 30 25 22 35 18Z"/></svg>'
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: 'blob-9',
|
|
2785
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M30 12C52 4 82 15 90 38C98 65 75 92 48 90C22 88 8 62 15 38C20 25 22 20 30 12Z"/></svg>'
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
name: 'blob-10',
|
|
2789
|
+
data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M34 16C55 5 80 15 88 36C95 60 78 90 50 92C22 95 8 65 15 42C20 30 25 22 34 16Z"/></svg>'
|
|
2790
|
+
}
|
|
2791
|
+
];
|
|
2792
|
+
|
|
2793
|
+
const PRESET_CATEGORIES = [
|
|
2794
|
+
{
|
|
2795
|
+
name: 'Instagram',
|
|
2796
|
+
icon: 'fluent:camera-24-regular',
|
|
2797
|
+
presets: [
|
|
2798
|
+
{ name: 'Post', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
|
|
2799
|
+
{ name: 'Story', width: 1080, height: 1920, icon: 'fluent:video-clip-24-regular' },
|
|
2800
|
+
{ name: 'Ad', width: 1080, height: 1080, icon: 'fluent:megaphone-24-regular' },
|
|
2801
|
+
]
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
name: 'Facebook',
|
|
2805
|
+
icon: 'fluent:people-24-regular',
|
|
2806
|
+
presets: [
|
|
2807
|
+
{ name: 'Post (Landscape)', width: 1200, height: 630, icon: 'fluent:image-24-regular' },
|
|
2808
|
+
{ name: 'Post (Square)', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
|
|
2809
|
+
{ name: 'Cover', width: 851, height: 315, icon: 'fluent:image-24-regular' },
|
|
2810
|
+
]
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
name: 'Youtube',
|
|
2814
|
+
icon: 'fluent:video-24-regular',
|
|
2815
|
+
presets: [
|
|
2816
|
+
{ name: 'Thumbnail', width: 1280, height: 720, icon: 'fluent:image-24-regular' },
|
|
2817
|
+
{ name: 'Channel', width: 2560, height: 1440, icon: 'fluent:image-24-regular' },
|
|
2818
|
+
{ name: 'Short', width: 1080, height: 1920, icon: 'fluent:video-clip-24-regular' },
|
|
2819
|
+
]
|
|
2820
|
+
},
|
|
2821
|
+
{
|
|
2822
|
+
name: 'LinkedIn',
|
|
2823
|
+
icon: 'fluent:briefcase-24-regular',
|
|
2824
|
+
presets: [
|
|
2825
|
+
{ name: 'Post', width: 1200, height: 627, icon: 'fluent:image-24-regular' },
|
|
2826
|
+
{ name: 'Banner', width: 1584, height: 396, icon: 'fluent:image-24-regular' },
|
|
2827
|
+
{ name: 'Square', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
|
|
2828
|
+
]
|
|
2829
|
+
},
|
|
2830
|
+
{
|
|
2831
|
+
name: 'Twitter',
|
|
2832
|
+
icon: 'fluent:news-24-regular',
|
|
2833
|
+
presets: [
|
|
2834
|
+
{ name: 'Post', width: 1600, height: 900, icon: 'fluent:image-24-regular' },
|
|
2835
|
+
{ name: 'Header', width: 1500, height: 500, icon: 'fluent:image-24-regular' },
|
|
2836
|
+
{ name: 'Square', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
|
|
2837
|
+
]
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
name: 'Video',
|
|
2841
|
+
icon: 'fluent:video-24-regular',
|
|
2842
|
+
presets: [
|
|
2843
|
+
{ name: 'Full HD', width: 1920, height: 1080, icon: 'fluent:video-24-regular' },
|
|
2844
|
+
{ name: '4K UHD', width: 3840, height: 2160, icon: 'fluent:video-24-regular' },
|
|
2845
|
+
{ name: 'Vertical HD', width: 1080, height: 1920, icon: 'fluent:video-24-regular' },
|
|
2846
|
+
{ name: 'Square HD', width: 1080, height: 1080, icon: 'fluent:video-24-regular' },
|
|
2847
|
+
]
|
|
2848
|
+
}
|
|
2849
|
+
];
|
|
2850
|
+
|
|
2851
|
+
const IMAGE_DESIGNER = new InjectionToken('IMAGE_DESIGNER');
|
|
2852
|
+
|
|
2853
|
+
class Settings {
|
|
2854
|
+
imageDesigner = inject(IMAGE_DESIGNER, { optional: true });
|
|
2855
|
+
designerService = inject(ImageDesignerService);
|
|
2856
|
+
selectedLayerId = this.designerService.selectedLayerId;
|
|
2857
|
+
layers = this.designerService.layers;
|
|
2858
|
+
fonts = this.designerService.fonts;
|
|
2859
|
+
selectedLayer = computed(() => {
|
|
2860
|
+
const id = this.selectedLayerId();
|
|
2861
|
+
if (!id)
|
|
2862
|
+
return null;
|
|
2863
|
+
return this.layers().find(l => l.id === id);
|
|
2864
|
+
}, ...(ngDevMode ? [{ debugName: "selectedLayer" }] : /* istanbul ignore next */ []));
|
|
2865
|
+
showColorPicker = computed(() => {
|
|
2866
|
+
const layer = this.selectedLayer();
|
|
2867
|
+
if (!layer)
|
|
2868
|
+
return false;
|
|
2869
|
+
return ['text', 'shape', 'pattern'].includes(layer.type);
|
|
2870
|
+
}, ...(ngDevMode ? [{ debugName: "showColorPicker" }] : /* istanbul ignore next */ []));
|
|
2871
|
+
presetColors = signal([
|
|
2872
|
+
'#000000', '#7a7a7a', '#ffffff',
|
|
2873
|
+
'#f44336', '#e91e63', '#9c27b0',
|
|
2874
|
+
'#673ab7', '#3f51b5', '#2196f3',
|
|
2875
|
+
'#03a9f4', '#00bcd4', '#009688',
|
|
2876
|
+
'#4caf50', '#8bc34a', '#cddc39',
|
|
2877
|
+
'#ffeb3b', '#ffc107', '#ff9800',
|
|
2878
|
+
'#ff5722', '#795548', '#607d8b'
|
|
2879
|
+
], ...(ngDevMode ? [{ debugName: "presetColors" }] : /* istanbul ignore next */ []));
|
|
2880
|
+
fontWeightNames = {
|
|
2881
|
+
100: 'Thin',
|
|
2882
|
+
200: 'Extra Light',
|
|
2883
|
+
300: 'Light',
|
|
2884
|
+
400: 'Regular',
|
|
2885
|
+
500: 'Medium',
|
|
2886
|
+
600: 'Semi Bold',
|
|
2887
|
+
700: 'Bold',
|
|
2888
|
+
800: 'Extra Bold',
|
|
2889
|
+
900: 'Black'
|
|
2890
|
+
};
|
|
2891
|
+
getFontWeightName(value) {
|
|
2892
|
+
const numericValue = value === 'bold' ? 700 : (value === 'normal' ? 400 : (parseInt(value, 10) || 400));
|
|
2893
|
+
return this.fontWeightNames[numericValue] || numericValue.toString();
|
|
2894
|
+
}
|
|
2895
|
+
async updateOpacity(value) {
|
|
2896
|
+
const id = this.selectedLayerId();
|
|
2897
|
+
if (id) {
|
|
2898
|
+
await this.designerService.updateLayer(id, { opacity: value });
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
async updateColor(color) {
|
|
2902
|
+
const id = this.selectedLayerId();
|
|
2903
|
+
const layer = this.selectedLayer();
|
|
2904
|
+
if (id && layer) {
|
|
2905
|
+
await this.designerService.updateLayer(id, { fill: color, type: layer.type });
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
async updateFont(font) {
|
|
2909
|
+
const id = this.selectedLayerId();
|
|
2910
|
+
if (id) {
|
|
2911
|
+
await this.designerService.loadFont(font);
|
|
2912
|
+
await this.designerService.updateLayer(id, { fontFamily: font });
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
async updateFontWeight(value) {
|
|
2916
|
+
const id = this.selectedLayerId();
|
|
2917
|
+
if (id) {
|
|
2918
|
+
await this.designerService.updateLayer(id, { fontWeight: value });
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
async updateTextStyle(style) {
|
|
2922
|
+
const id = this.selectedLayerId();
|
|
2923
|
+
if (!id)
|
|
2924
|
+
return;
|
|
2925
|
+
const decorations = [];
|
|
2926
|
+
if (style.includes('underline'))
|
|
2927
|
+
decorations.push('underline');
|
|
2928
|
+
if (style.includes('line-through'))
|
|
2929
|
+
decorations.push('line-through');
|
|
2930
|
+
const config = {
|
|
2931
|
+
fontStyle: style.includes('italic') ? 'italic' : 'normal',
|
|
2932
|
+
textDecoration: decorations.join(' ') || 'none'
|
|
2933
|
+
};
|
|
2934
|
+
await this.designerService.updateLayer(id, config);
|
|
2935
|
+
}
|
|
2936
|
+
async toggleUppercase() {
|
|
2937
|
+
const id = this.selectedLayerId();
|
|
2938
|
+
const layer = this.selectedLayer();
|
|
2939
|
+
if (id && layer) {
|
|
2940
|
+
const currentCase = layer['textCase'];
|
|
2941
|
+
const newCase = currentCase === 'upper' ? 'normal' : 'upper';
|
|
2942
|
+
await this.designerService.updateLayer(id, { textCase: newCase });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
async updateTextAlign(align) {
|
|
2946
|
+
const id = this.selectedLayerId();
|
|
2947
|
+
if (id) {
|
|
2948
|
+
await this.designerService.updateLayer(id, { align });
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
async updateLineHeight(value) {
|
|
2952
|
+
const id = this.selectedLayerId();
|
|
2953
|
+
if (id) {
|
|
2954
|
+
await this.designerService.updateLayer(id, { lineHeight: value });
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
async updateLetterSpacing(value) {
|
|
2958
|
+
const id = this.selectedLayerId();
|
|
2959
|
+
if (id) {
|
|
2960
|
+
await this.designerService.updateLayer(id, { letterSpacing: value });
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
getTextStyle = computed(() => {
|
|
2964
|
+
const layer = this.selectedLayer();
|
|
2965
|
+
if (!layer)
|
|
2966
|
+
return [];
|
|
2967
|
+
const styles = [];
|
|
2968
|
+
if (layer['fontStyle'] === 'italic')
|
|
2969
|
+
styles.push('italic');
|
|
2970
|
+
const decoration = layer['textDecoration'] || '';
|
|
2971
|
+
if (decoration.includes('underline'))
|
|
2972
|
+
styles.push('underline');
|
|
2973
|
+
if (decoration.includes('line-through'))
|
|
2974
|
+
styles.push('line-through');
|
|
2975
|
+
return styles;
|
|
2976
|
+
}, { ...(ngDevMode ? { debugName: "getTextStyle" } : /* istanbul ignore next */ {}), equal: (a, b) => {
|
|
2977
|
+
if (a.length !== b.length)
|
|
2978
|
+
return false;
|
|
2979
|
+
return a.every((v, i) => v === b[i]);
|
|
2980
|
+
} });
|
|
2981
|
+
toggleLock() {
|
|
2982
|
+
const id = this.selectedLayerId();
|
|
2983
|
+
if (id) {
|
|
2984
|
+
this.designerService.toggleLayerLock(id);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
async flipHorizontal() {
|
|
2988
|
+
const id = this.selectedLayerId();
|
|
2989
|
+
if (id) {
|
|
2990
|
+
await this.designerService.flipHorizontal(id);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
async flipVertical() {
|
|
2994
|
+
const id = this.selectedLayerId();
|
|
2995
|
+
if (id) {
|
|
2996
|
+
await this.designerService.flipVertical(id);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
async fitToPage() {
|
|
3000
|
+
const id = this.selectedLayerId();
|
|
3001
|
+
if (id) {
|
|
3002
|
+
await this.designerService.fitToPage(id);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
async fillPage() {
|
|
3006
|
+
const id = this.selectedLayerId();
|
|
3007
|
+
if (id) {
|
|
3008
|
+
await this.designerService.fillPage(id);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
saveSnapshot() {
|
|
3012
|
+
const snapshot = this.designerService.getSnapshot();
|
|
3013
|
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(snapshot));
|
|
3014
|
+
const downloadAnchorNode = document.createElement('a');
|
|
3015
|
+
downloadAnchorNode.setAttribute("href", dataStr);
|
|
3016
|
+
downloadAnchorNode.setAttribute("download", "design-snapshot.json");
|
|
3017
|
+
document.body.appendChild(downloadAnchorNode);
|
|
3018
|
+
downloadAnchorNode.click();
|
|
3019
|
+
downloadAnchorNode.remove();
|
|
3020
|
+
}
|
|
3021
|
+
loadSnapshot(event) {
|
|
3022
|
+
const input = event.target;
|
|
3023
|
+
if (input.files && input.files[0]) {
|
|
3024
|
+
const reader = new FileReader();
|
|
3025
|
+
reader.onload = async (e) => {
|
|
3026
|
+
try {
|
|
3027
|
+
const snapshot = JSON.parse(e.target?.result);
|
|
3028
|
+
await this.designerService.loadSnapshot(snapshot);
|
|
3029
|
+
}
|
|
3030
|
+
catch (error) {
|
|
3031
|
+
console.error('Failed to load snapshot:', error);
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
reader.readAsText(input.files[0]);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
openEffects() {
|
|
3038
|
+
this.imageDesigner?.openEffects();
|
|
3039
|
+
}
|
|
3040
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Settings, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3041
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: Settings, isStandalone: true, selector: "ngs-settings", ngImport: i0, template: "@let layer = selectedLayer();\n\n@if (layer) {\n <ngs-toolbar\n class=\"h-14 z-10 bg-surface-container-lowest border-b border-border px-5 absolute top-0 left-0 right-0\">\n <!-- Color -->\n @if (showColorPicker()) {\n <ngs-toolbar-item>\n <button [ngsPopoverTriggerFor]=\"colorPopover\"\n position=\"below-center\"\n class=\"size-7 cursor-pointer hover:ring-2 hover:ring-primary rounded-full border border-border\"\n ngsRipple\n [style.backgroundColor]=\"layer['fill'] || '#000000'\">\n </button>\n <ngs-popover #colorPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-72\">\n <div class=\"text-xs font-medium uppercase text-muted-foreground mb-4\">\n {{ layer.type === 'text' ? 'Text Color' : 'Fill Color' }}\n </div>\n <ngs-color-switcher [colors]=\"presetColors()\"\n [selectedColor]=\"layer['fill'] || '#000000'\"\n (colorChange)=\"updateColor($event)\"/>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n <ngs-divider vertical/>\n }\n\n <!-- Opacity -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"opacityPopover\"\n position=\"below-center\">\n <ngs-icon name=\"fluent:blur-24-regular\"/>\n </button>\n <ngs-popover #opacityPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Opacity</label>\n <span class=\"text-xs text-muted-foreground\">{{ (layer['opacity'] ?? 1) * 100 | number:'1.0-0' }}%</span>\n </div>\n <ngs-slider [min]=\"0\" [max]=\"1\" [step]=\"0.01\">\n <input ngsSliderThumb\n [value]=\"layer['opacity'] ?? 1\"\n (valueChange)=\"updateOpacity($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Font Selection for text layers -->\n @if (layer.type === 'text') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fontMenu\">\n <span class=\"truncate max-w-[100px]\">{{ layer['fontFamily'] }}</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fontMenu=\"ngsMenu\">\n <div class=\"min-w-[200px]\">\n @for (font of fonts(); track font) {\n <button ngs-menu-item\n [selected]=\"layer['fontFamily'] === font\"\n (click)=\"updateFont(font)\">\n {{ font }}\n </button>\n }\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <!-- Text Styles -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"boldPopover\"\n position=\"below-center\"\n [class.text-primary]=\"layer['fontWeight'] && layer['fontWeight'] !== 'normal' && layer['fontWeight'] !== 400\"\n title=\"Bold\">\n <ngs-icon name=\"fluent:text-bold-24-regular\"/>\n </button>\n <ngs-popover #boldPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Font Weight</label>\n <span class=\"text-xs text-muted-foreground\">{{ getFontWeightName(layer['fontWeight']) }}</span>\n </div>\n <ngs-slider [min]=\"300\" [max]=\"900\" [step]=\"100\">\n <input ngsSliderThumb\n [value]=\"layer['fontWeight'] === 'bold' ? 700 : (layer['fontWeight'] === 'normal' ? 400 : (layer['fontWeight'] || 400))\"\n (valueChange)=\"updateFontWeight($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Uppercase -->\n <ngs-toolbar-item>\n <button ngsIconButton\n (click)=\"toggleUppercase()\"\n [class.text-primary]=\"layer['textCase'] === 'upper'\"\n title=\"Uppercase\">\n <ngs-icon name=\"fluent:text-case-uppercase-24-regular\"/>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <ngs-button-toggle-group multiple\n hideSelectionIndicator\n [value]=\"getTextStyle()\"\n (valueChange)=\"updateTextStyle($event)\">\n <ngs-button-toggle value=\"italic\" title=\"Italic\">\n <ngs-icon name=\"fluent:text-italic-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"underline\" title=\"Underline\">\n <ngs-icon name=\"fluent:text-underline-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"line-through\" title=\"Strikethrough\">\n <ngs-icon name=\"fluent:text-strikethrough-24-regular\"/>\n </ngs-button-toggle>\n </ngs-button-toggle-group>\n </ngs-toolbar-item>\n\n <!-- Text Alignment -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"alignPopover\"\n position=\"below-center\"\n title=\"Alignment\">\n <ngs-icon [name]=\"'fluent:text-align-' + (layer['align'] || 'left') + '-24-regular'\"/>\n </button>\n <ngs-popover #alignPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-2 flex gap-1\">\n <button ngsIconButton (click)=\"updateTextAlign('left')\" [class.text-primary]=\"layer['align'] === 'left'\">\n <ngs-icon name=\"fluent:text-align-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('center')\" [class.text-primary]=\"layer['align'] === 'center'\">\n <ngs-icon name=\"fluent:text-align-center-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('right')\" [class.text-primary]=\"layer['align'] === 'right'\">\n <ngs-icon name=\"fluent:text-align-right-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('justify')\" [class.text-primary]=\"layer['align'] === 'justify'\">\n <ngs-icon name=\"fluent:text-align-justify-24-regular\"/>\n </button>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Spacing (Line Height & Letter Spacing) -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"spacingPopover\"\n position=\"below-center\"\n title=\"Spacing\">\n <ngs-icon name=\"fluent:text-line-spacing-24-regular\"/>\n </button>\n <ngs-popover #spacingPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-6\">\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Line Height</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['lineHeight'] || 1.2 | number:'1.1-1' }}</span>\n </div>\n <ngs-slider [min]=\"0.5\" [max]=\"3\" [step]=\"0.1\">\n <input ngsSliderThumb\n [value]=\"layer['lineHeight'] || 1.2\"\n (valueChange)=\"updateLineHeight($event)\"/>\n </ngs-slider>\n </div>\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Letter Spacing</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['letterSpacing'] || 0 }}</span>\n </div>\n <ngs-slider [min]=\"-5\" [max]=\"50\" [step]=\"1\">\n <input ngsSliderThumb\n [value]=\"layer['letterSpacing'] || 0\"\n (valueChange)=\"updateLetterSpacing($event)\"/>\n </ngs-slider>\n </div>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n }\n\n @if (layer.type === 'image') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"flipMenu\">\n <span>Flip</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #flipMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"flipHorizontal()\">\n <ngs-icon name=\"fluent:flip-horizontal-24-regular\" class=\"mr-2\"/>\n <span>Flip horizontally</span>\n </button>\n <button ngs-menu-item (click)=\"flipVertical()\">\n <ngs-icon name=\"fluent:flip-vertical-24-regular\" class=\"mr-2\"/>\n <span>Flip vertically</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fitMenu\">\n <span>Fit to</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fitMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"fitToPage()\">\n <span>Fit to page</span>\n </button>\n <button ngs-menu-item (click)=\"fillPage()\">\n <span>Fill page</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n }\n\n <ngs-toolbar-item>\n <button ngsButton reverse (click)=\"openEffects()\">\n <span>Effects</span>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-spacer/>\n\n <!-- <ngs-divider vertical/>-->\n\n <ngs-toolbar-item>\n <button ngsIconButton (click)=\"toggleLock()\">\n <ngs-icon [name]=\"layer['locked'] ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"/>\n </button>\n </ngs-toolbar-item>\n\n </ngs-toolbar>\n}\n", styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ColorSwitcher, selector: "ngs-color-switcher", inputs: ["colors", "selectedColor", "disabled"], outputs: ["colorChange"], exportAs: ["ngsColorSwitcher"] }, { kind: "component", type: Slider, selector: "ngs-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "max", "step", "displayWith"], exportAs: ["ngsSlider"] }, { kind: "directive", type: SliderThumb, selector: "input[ngsSliderThumb]", inputs: ["value"], outputs: ["valueChange"], exportAs: ["ngsSliderThumb"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: ToolbarItem, selector: "ngs-toolbar-item", inputs: ["hidden"], outputs: ["hiddenChange"] }, { kind: "component", type: Popover, selector: "ngs-popover", exportAs: ["ngsPopover"] }, { kind: "directive", type: PopoverContent, selector: "[ngsPopoverContent]" }, { kind: "directive", type: PopoverTriggerForDirective, selector: "[ngsPopoverTriggerFor]", inputs: ["ngsPopoverTriggerFor", "ngsPopoverContext", "trigger", "position", "delay", "origin", "closeOnOriginClick", "closeOnOriginMouseLeave", "hasBackdrop"], outputs: ["opened", "closed"], exportAs: ["ngsPopoverTriggerFor"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: Divider, selector: "ngs-divider", inputs: ["vertical", "inset", "fixedHeight"], exportAs: ["ngsDivider"] }, { kind: "component", type: Menu, selector: "ngs-menu", inputs: ["role", "classList", "xPosition", "yPosition"], outputs: ["closed"], exportAs: ["ngsMenu"] }, { kind: "component", type: MenuItem, selector: "ngs-menu-item, [ngs-menu-item]", inputs: ["disabled", "role", "selected"], outputs: ["_triggered"], exportAs: ["ngsMenuItem"] }, { kind: "directive", type: MenuTrigger, selector: "[ngsMenuTriggerFor]", inputs: ["ngsMenuTriggerFor", "ngsMenuTriggerData", "ngsMenuDisabled", "xPosition", "yPosition", "ngsMenuTriggerRestoreFocus"], outputs: ["menuOpened", "menuClosed"], exportAs: ["ngsMenuTrigger"] }, { kind: "directive", type: Ripple, selector: "[ngsRipple]", inputs: ["ngsRippleColor", "ngsRippleUnbounded", "ngsRippleCentered", "ngsRippleRadius", "ngsRippleAnimation", "ngsRippleDisabled", "ngsRippleTrigger"], outputs: ["ngsRippleCenteredChange", "ngsRippleDisabledChange", "ngsRippleTriggerChange"], exportAs: ["ngsRipple"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: ButtonToggle, selector: "ngs-button-toggle", inputs: ["id", "value", "name", "checked", "disabled"], outputs: ["change"] }, { kind: "component", type: ButtonToggleGroup, selector: "ngs-button-toggle-group", inputs: ["appearance", "disabled", "multiple", "hideSelectionIndicator", "vertical", "value"], outputs: ["valueChange", "change"], exportAs: ["ngsButtonToggleGroup"] }, { kind: "ngmodule", type: FormsModule }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3042
|
+
}
|
|
3043
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Settings, decorators: [{
|
|
3044
|
+
type: Component,
|
|
3045
|
+
args: [{ selector: 'ngs-settings', imports: [
|
|
3046
|
+
CommonModule,
|
|
3047
|
+
ColorSwitcher,
|
|
3048
|
+
Slider,
|
|
3049
|
+
SliderThumb,
|
|
3050
|
+
Toolbar,
|
|
3051
|
+
ToolbarItem,
|
|
3052
|
+
Popover,
|
|
3053
|
+
PopoverContent,
|
|
3054
|
+
PopoverTriggerForDirective,
|
|
3055
|
+
Button,
|
|
3056
|
+
Icon,
|
|
3057
|
+
Divider,
|
|
3058
|
+
DecimalPipe,
|
|
3059
|
+
Menu,
|
|
3060
|
+
MenuItem,
|
|
3061
|
+
MenuTrigger,
|
|
3062
|
+
Ripple,
|
|
3063
|
+
ToolbarSpacer,
|
|
3064
|
+
ButtonToggle,
|
|
3065
|
+
ButtonToggleGroup,
|
|
3066
|
+
FormsModule
|
|
3067
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let layer = selectedLayer();\n\n@if (layer) {\n <ngs-toolbar\n class=\"h-14 z-10 bg-surface-container-lowest border-b border-border px-5 absolute top-0 left-0 right-0\">\n <!-- Color -->\n @if (showColorPicker()) {\n <ngs-toolbar-item>\n <button [ngsPopoverTriggerFor]=\"colorPopover\"\n position=\"below-center\"\n class=\"size-7 cursor-pointer hover:ring-2 hover:ring-primary rounded-full border border-border\"\n ngsRipple\n [style.backgroundColor]=\"layer['fill'] || '#000000'\">\n </button>\n <ngs-popover #colorPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-72\">\n <div class=\"text-xs font-medium uppercase text-muted-foreground mb-4\">\n {{ layer.type === 'text' ? 'Text Color' : 'Fill Color' }}\n </div>\n <ngs-color-switcher [colors]=\"presetColors()\"\n [selectedColor]=\"layer['fill'] || '#000000'\"\n (colorChange)=\"updateColor($event)\"/>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n <ngs-divider vertical/>\n }\n\n <!-- Opacity -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"opacityPopover\"\n position=\"below-center\">\n <ngs-icon name=\"fluent:blur-24-regular\"/>\n </button>\n <ngs-popover #opacityPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Opacity</label>\n <span class=\"text-xs text-muted-foreground\">{{ (layer['opacity'] ?? 1) * 100 | number:'1.0-0' }}%</span>\n </div>\n <ngs-slider [min]=\"0\" [max]=\"1\" [step]=\"0.01\">\n <input ngsSliderThumb\n [value]=\"layer['opacity'] ?? 1\"\n (valueChange)=\"updateOpacity($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Font Selection for text layers -->\n @if (layer.type === 'text') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fontMenu\">\n <span class=\"truncate max-w-[100px]\">{{ layer['fontFamily'] }}</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fontMenu=\"ngsMenu\">\n <div class=\"min-w-[200px]\">\n @for (font of fonts(); track font) {\n <button ngs-menu-item\n [selected]=\"layer['fontFamily'] === font\"\n (click)=\"updateFont(font)\">\n {{ font }}\n </button>\n }\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <!-- Text Styles -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"boldPopover\"\n position=\"below-center\"\n [class.text-primary]=\"layer['fontWeight'] && layer['fontWeight'] !== 'normal' && layer['fontWeight'] !== 400\"\n title=\"Bold\">\n <ngs-icon name=\"fluent:text-bold-24-regular\"/>\n </button>\n <ngs-popover #boldPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Font Weight</label>\n <span class=\"text-xs text-muted-foreground\">{{ getFontWeightName(layer['fontWeight']) }}</span>\n </div>\n <ngs-slider [min]=\"300\" [max]=\"900\" [step]=\"100\">\n <input ngsSliderThumb\n [value]=\"layer['fontWeight'] === 'bold' ? 700 : (layer['fontWeight'] === 'normal' ? 400 : (layer['fontWeight'] || 400))\"\n (valueChange)=\"updateFontWeight($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Uppercase -->\n <ngs-toolbar-item>\n <button ngsIconButton\n (click)=\"toggleUppercase()\"\n [class.text-primary]=\"layer['textCase'] === 'upper'\"\n title=\"Uppercase\">\n <ngs-icon name=\"fluent:text-case-uppercase-24-regular\"/>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <ngs-button-toggle-group multiple\n hideSelectionIndicator\n [value]=\"getTextStyle()\"\n (valueChange)=\"updateTextStyle($event)\">\n <ngs-button-toggle value=\"italic\" title=\"Italic\">\n <ngs-icon name=\"fluent:text-italic-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"underline\" title=\"Underline\">\n <ngs-icon name=\"fluent:text-underline-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"line-through\" title=\"Strikethrough\">\n <ngs-icon name=\"fluent:text-strikethrough-24-regular\"/>\n </ngs-button-toggle>\n </ngs-button-toggle-group>\n </ngs-toolbar-item>\n\n <!-- Text Alignment -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"alignPopover\"\n position=\"below-center\"\n title=\"Alignment\">\n <ngs-icon [name]=\"'fluent:text-align-' + (layer['align'] || 'left') + '-24-regular'\"/>\n </button>\n <ngs-popover #alignPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-2 flex gap-1\">\n <button ngsIconButton (click)=\"updateTextAlign('left')\" [class.text-primary]=\"layer['align'] === 'left'\">\n <ngs-icon name=\"fluent:text-align-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('center')\" [class.text-primary]=\"layer['align'] === 'center'\">\n <ngs-icon name=\"fluent:text-align-center-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('right')\" [class.text-primary]=\"layer['align'] === 'right'\">\n <ngs-icon name=\"fluent:text-align-right-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('justify')\" [class.text-primary]=\"layer['align'] === 'justify'\">\n <ngs-icon name=\"fluent:text-align-justify-24-regular\"/>\n </button>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Spacing (Line Height & Letter Spacing) -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"spacingPopover\"\n position=\"below-center\"\n title=\"Spacing\">\n <ngs-icon name=\"fluent:text-line-spacing-24-regular\"/>\n </button>\n <ngs-popover #spacingPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-6\">\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Line Height</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['lineHeight'] || 1.2 | number:'1.1-1' }}</span>\n </div>\n <ngs-slider [min]=\"0.5\" [max]=\"3\" [step]=\"0.1\">\n <input ngsSliderThumb\n [value]=\"layer['lineHeight'] || 1.2\"\n (valueChange)=\"updateLineHeight($event)\"/>\n </ngs-slider>\n </div>\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Letter Spacing</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['letterSpacing'] || 0 }}</span>\n </div>\n <ngs-slider [min]=\"-5\" [max]=\"50\" [step]=\"1\">\n <input ngsSliderThumb\n [value]=\"layer['letterSpacing'] || 0\"\n (valueChange)=\"updateLetterSpacing($event)\"/>\n </ngs-slider>\n </div>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n }\n\n @if (layer.type === 'image') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"flipMenu\">\n <span>Flip</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #flipMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"flipHorizontal()\">\n <ngs-icon name=\"fluent:flip-horizontal-24-regular\" class=\"mr-2\"/>\n <span>Flip horizontally</span>\n </button>\n <button ngs-menu-item (click)=\"flipVertical()\">\n <ngs-icon name=\"fluent:flip-vertical-24-regular\" class=\"mr-2\"/>\n <span>Flip vertically</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fitMenu\">\n <span>Fit to</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fitMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"fitToPage()\">\n <span>Fit to page</span>\n </button>\n <button ngs-menu-item (click)=\"fillPage()\">\n <span>Fill page</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n }\n\n <ngs-toolbar-item>\n <button ngsButton reverse (click)=\"openEffects()\">\n <span>Effects</span>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-spacer/>\n\n <!-- <ngs-divider vertical/>-->\n\n <ngs-toolbar-item>\n <button ngsIconButton (click)=\"toggleLock()\">\n <ngs-icon [name]=\"layer['locked'] ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"/>\n </button>\n </ngs-toolbar-item>\n\n </ngs-toolbar>\n}\n", styles: [":host{display:contents}\n"] }]
|
|
3068
|
+
}] });
|
|
3069
|
+
|
|
3070
|
+
class Effects {
|
|
3071
|
+
designerService = inject(ImageDesignerService);
|
|
3072
|
+
imageDesigner = inject(IMAGE_DESIGNER);
|
|
3073
|
+
selectedLayerId = this.designerService.selectedLayerId;
|
|
3074
|
+
layers = this.designerService.layers;
|
|
3075
|
+
selectedLayer = computed(() => {
|
|
3076
|
+
const id = this.selectedLayerId();
|
|
3077
|
+
if (!id)
|
|
3078
|
+
return null;
|
|
3079
|
+
return this.layers().find(l => l.id === id);
|
|
3080
|
+
}, ...(ngDevMode ? [{ debugName: "selectedLayer" }] : /* istanbul ignore next */ []));
|
|
3081
|
+
defaultThumb = 'https://picsum.photos/id/11/200/200';
|
|
3082
|
+
selectedLayerThumb = computed(() => {
|
|
3083
|
+
const id = this.selectedLayerId();
|
|
3084
|
+
if (!id)
|
|
3085
|
+
return this.defaultThumb;
|
|
3086
|
+
return this.designerService.getLayerThumbnail(id) || this.defaultThumb;
|
|
3087
|
+
}, ...(ngDevMode ? [{ debugName: "selectedLayerThumb" }] : /* istanbul ignore next */ []));
|
|
3088
|
+
effects = computed(() => {
|
|
3089
|
+
const id = this.selectedLayerId();
|
|
3090
|
+
const thumb = this.selectedLayerThumb();
|
|
3091
|
+
if (!id) {
|
|
3092
|
+
return [
|
|
3093
|
+
{ id: 'none', name: 'Original', thumb },
|
|
3094
|
+
{ id: 'grayscale', name: 'Grayscale', thumb },
|
|
3095
|
+
{ id: 'sepia', name: 'Sepia', thumb },
|
|
3096
|
+
{ id: 'cold', name: 'Cold', thumb },
|
|
3097
|
+
{ id: 'warm', name: 'Warm', thumb },
|
|
3098
|
+
];
|
|
3099
|
+
}
|
|
3100
|
+
return [
|
|
3101
|
+
{ id: 'none', name: 'Original', thumb: this.designerService.getLayerThumbnail(id, 'none') || thumb },
|
|
3102
|
+
{ id: 'grayscale', name: 'Grayscale', thumb: this.designerService.getLayerThumbnail(id, 'grayscale') || thumb },
|
|
3103
|
+
{ id: 'sepia', name: 'Sepia', thumb: this.designerService.getLayerThumbnail(id, 'sepia') || thumb },
|
|
3104
|
+
{ id: 'cold', name: 'Cold', thumb: this.designerService.getLayerThumbnail(id, 'cold') || thumb },
|
|
3105
|
+
{ id: 'warm', name: 'Warm', thumb: this.designerService.getLayerThumbnail(id, 'warm') || thumb },
|
|
3106
|
+
];
|
|
3107
|
+
}, ...(ngDevMode ? [{ debugName: "effects" }] : /* istanbul ignore next */ []));
|
|
3108
|
+
adjustments = signal([
|
|
3109
|
+
{ id: 'blur', name: 'Blur', min: 0, max: 20, step: 1, default: 0 },
|
|
3110
|
+
// { id: 'brightness', name: 'Brightness', min: -1, max: 1, step: 0.1, default: 0 },
|
|
3111
|
+
// { id: 'temperature', name: 'Temperature', min: -100, max: 100, step: 1, default: 0 },
|
|
3112
|
+
// { id: 'contrast', name: 'Contrast', min: -100, max: 100, step: 1, default: 0 },
|
|
3113
|
+
// { id: 'white', name: 'White', min: -100, max: 100, step: 1, default: 0 },
|
|
3114
|
+
// { id: 'black', name: 'Black', min: -100, max: 100, step: 1, default: 0 },
|
|
3115
|
+
// { id: 'vibrance', name: 'Vibrance', min: -100, max: 100, step: 1, default: 0 },
|
|
3116
|
+
// { id: 'saturation', name: 'Saturation', min: -100, max: 100, step: 1, default: 0 },
|
|
3117
|
+
{ id: 'border', name: 'Border', min: 0, max: 50, step: 1, default: 0 },
|
|
3118
|
+
{ id: 'cornerRadius', name: 'Corner Radius', min: 0, max: 100, step: 1, default: 0 },
|
|
3119
|
+
{ id: 'shadow', name: 'Shadow', isGroup: true, controls: [
|
|
3120
|
+
{ id: 'shadowBlur', name: 'Blur', min: 0, max: 100, step: 1, default: 15 },
|
|
3121
|
+
{ id: 'shadowOffsetX', name: 'Offset X', min: -100, max: 100, step: 1, default: 0 },
|
|
3122
|
+
{ id: 'shadowOffsetY', name: 'Offset Y', min: -100, max: 100, step: 1, default: 0 },
|
|
3123
|
+
{ id: 'shadowOpacity', name: 'Opacity', min: 0, max: 100, step: 1, default: 100 },
|
|
3124
|
+
{ id: 'shadowColor', name: 'Color', type: 'color', default: '#000000' }
|
|
3125
|
+
] }
|
|
3126
|
+
], ...(ngDevMode ? [{ debugName: "adjustments" }] : /* istanbul ignore next */ []));
|
|
3127
|
+
async applyEffect(effectId) {
|
|
3128
|
+
const id = this.selectedLayerId();
|
|
3129
|
+
if (id) {
|
|
3130
|
+
await this.designerService.updateLayer(id, { effect: effectId });
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
async resetAll() {
|
|
3134
|
+
const id = this.selectedLayerId();
|
|
3135
|
+
if (id) {
|
|
3136
|
+
const config = { effect: 'none' };
|
|
3137
|
+
this.adjustments().forEach(adj => {
|
|
3138
|
+
config[`${adj.id}Enabled`] = false;
|
|
3139
|
+
if (adj.isGroup) {
|
|
3140
|
+
adj.controls.forEach(ctrl => {
|
|
3141
|
+
config[ctrl.id] = ctrl.default;
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
else {
|
|
3145
|
+
config[adj.id] = adj.default;
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
await this.designerService.updateLayer(id, config);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
async updateAdjustment(id, value) {
|
|
3152
|
+
const layerId = this.selectedLayerId();
|
|
3153
|
+
if (layerId) {
|
|
3154
|
+
await this.designerService.updateLayer(layerId, { [id]: value });
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
async toggleAdjustment(id, enabled) {
|
|
3158
|
+
const layerId = this.selectedLayerId();
|
|
3159
|
+
if (layerId) {
|
|
3160
|
+
const config = { [`${id}Enabled`]: enabled };
|
|
3161
|
+
if (!enabled) {
|
|
3162
|
+
const adj = this.adjustments().find(a => a.id === id);
|
|
3163
|
+
if (adj) {
|
|
3164
|
+
if (adj.isGroup) {
|
|
3165
|
+
adj.controls.forEach(control => {
|
|
3166
|
+
config[control.id] = control.default;
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
else {
|
|
3170
|
+
config[id] = adj.default;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
await this.designerService.updateLayer(layerId, config);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
close() {
|
|
3178
|
+
this.imageDesigner.effectsPortal.set(null);
|
|
3179
|
+
}
|
|
3180
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Effects, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3181
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: Effects, isStandalone: true, selector: "ngs-effects", ngImport: i0, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-header class=\"border-b border-border flex items-center ps-4 pe-2\">\n <ngs-toolbar class=\"h-full\">\n <span class=\"font-medium\">Effects</span>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"tonal\" (click)=\"resetAll()\" class=\"me-2 h-8 text-xs px-3\">\n Reset\n </button>\n <button ngsIconButton (click)=\"close()\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"p-0 overflow-y-auto\">\n <div class=\"p-4\">\n <!-- Preset Effects -->\n <div class=\"flex gap-4 pb-6 scrollbar-hide flex-wrap\">\n @for (effect of effects() || []; track effect.id) {\n <button (click)=\"applyEffect(effect.id)\"\n class=\"flex flex-col items-center gap-2 group min-w-[80px]\">\n <div class=\"w-20 h-20 rounded-xl overflow-hidden border-2 transition-all group-hover:border-primary\"\n [class.border-primary]=\"(selectedLayer()?.['effect'] || 'none') === effect.id\"\n [class.border-transparent]=\"(selectedLayer()?.['effect'] || 'none') !== effect.id\">\n <img [src]=\"effect.thumb\" class=\"w-full h-full object-cover\" [alt]=\"effect.name\">\n </div>\n <span class=\"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors\">\n {{ effect.name }}\n </span>\n </button>\n }\n </div>\n\n <!-- Adjustments -->\n <div class=\"flex flex-col gap-6\">\n @for (adj of adjustments() || []; track adj.id) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium\">{{ adj.name }}</span>\n <ngs-slide-toggle [checked]=\"selectedLayer()?.[adj.id + 'Enabled']\"\n (change)=\"toggleAdjustment(adj.id, $event.checked)\"/>\n </div>\n\n @if (selectedLayer()?.[adj.id + 'Enabled']) {\n <div class=\"flex flex-col gap-4\">\n @if (adj.isGroup) {\n @for (control of adj.controls || []; track control.id) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-muted-foreground\">{{ control.name }}</span>\n @if (control.type === 'color') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.[control.id] || control.default\"\n [ngsColorPickerTriggerFor]=\"colorPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #colorPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.[control.id] || control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\"/>\n </ng-template>\n } @else {\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[control.id] ?? control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\">\n }\n </div>\n @if (control.type !== 'color') {\n <ngs-slider [min]=\"control.min\"\n [max]=\"control.max\"\n [step]=\"control.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[control.id] ?? control.default\"\n (valueChange)=\"updateAdjustment(control.id, $event)\">\n </ngs-slider>\n }\n </div>\n }\n } @else {\n <div class=\"flex items-center gap-4\">\n @if (adj.id === 'border') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n [ngsColorPickerTriggerFor]=\"borderPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #borderPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n (ngModelChange)=\"updateAdjustment('borderColor', $event)\"/>\n </ng-template>\n }\n <ngs-slider class=\"flex-1\"\n [min]=\"adj.min\"\n [max]=\"adj.max\"\n [step]=\"adj.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (valueChange)=\"updateAdjustment(adj.id, $event)\">\n </ngs-slider>\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (ngModelChange)=\"updateAdjustment(adj.id, $event)\">\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;height:100%}:host .scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}:host .scrollbar-hide::-webkit-scrollbar{display:none}:host ngs-slider{--ngs-slider-track-height: 4px;--ngs-slider-thumb-size: 16px}:host input[type=number]::-webkit-inner-spin-button,:host input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ngs-color-picker-thumbnail{width:32px;height:32px;min-width:32px;background:var(--ngs-color-picker-thumbnail-bg, #000)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: SlideToggle, selector: "ngs-slide-toggle", inputs: ["id", "name", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "disabled", "disableRipple", "tabIndex", "hideIcon", "color", "checked"], outputs: ["disabledChange", "checkedChange", "change", "toggleChange"], exportAs: ["ngsSlideToggle"] }, { kind: "component", type: Slider, selector: "ngs-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "max", "step", "displayWith"], exportAs: ["ngsSlider"] }, { kind: "directive", type: SliderThumb, selector: "input[ngsSliderThumb]", inputs: ["value"], outputs: ["valueChange"], exportAs: ["ngsSliderThumb"] }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: ColorPicker, selector: "ngs-color-picker", inputs: ["color", "disabled", "asDropdown", "showOpacity", "resultFormat"], outputs: ["colorChange", "rawColorChange"], exportAs: ["ngsColorPicker"] }, { kind: "component", type: ColorPickerThumbnail, selector: "ngs-color-picker-thumbnail,[ngs-color-picker-thumbnail]", inputs: ["color"], exportAs: ["ngsColorPickerThumbnail"] }, { kind: "directive", type: ColorPickerTriggerForDirective, selector: "[ngsColorPickerTriggerFor]", inputs: ["ngsColorPickerTriggerFor", "position"], outputs: ["opened", "closed"], exportAs: ["ngsColorPickerTriggerFor"] }] });
|
|
3182
|
+
}
|
|
3183
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Effects, decorators: [{
|
|
3184
|
+
type: Component,
|
|
3185
|
+
args: [{ selector: 'ngs-effects', imports: [
|
|
3186
|
+
CommonModule,
|
|
3187
|
+
Panel,
|
|
3188
|
+
PanelHeader,
|
|
3189
|
+
PanelContent,
|
|
3190
|
+
Button,
|
|
3191
|
+
Icon,
|
|
3192
|
+
SlideToggle,
|
|
3193
|
+
Slider,
|
|
3194
|
+
SliderThumb,
|
|
3195
|
+
Input,
|
|
3196
|
+
FormsModule,
|
|
3197
|
+
Toolbar,
|
|
3198
|
+
ToolbarSpacer,
|
|
3199
|
+
ColorPicker,
|
|
3200
|
+
ColorPickerThumbnail,
|
|
3201
|
+
ColorPickerTriggerForDirective
|
|
3202
|
+
], template: "<ngs-panel class=\"h-full\">\n <ngs-panel-header class=\"border-b border-border flex items-center ps-4 pe-2\">\n <ngs-toolbar class=\"h-full\">\n <span class=\"font-medium\">Effects</span>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"tonal\" (click)=\"resetAll()\" class=\"me-2 h-8 text-xs px-3\">\n Reset\n </button>\n <button ngsIconButton (click)=\"close()\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"p-0 overflow-y-auto\">\n <div class=\"p-4\">\n <!-- Preset Effects -->\n <div class=\"flex gap-4 pb-6 scrollbar-hide flex-wrap\">\n @for (effect of effects() || []; track effect.id) {\n <button (click)=\"applyEffect(effect.id)\"\n class=\"flex flex-col items-center gap-2 group min-w-[80px]\">\n <div class=\"w-20 h-20 rounded-xl overflow-hidden border-2 transition-all group-hover:border-primary\"\n [class.border-primary]=\"(selectedLayer()?.['effect'] || 'none') === effect.id\"\n [class.border-transparent]=\"(selectedLayer()?.['effect'] || 'none') !== effect.id\">\n <img [src]=\"effect.thumb\" class=\"w-full h-full object-cover\" [alt]=\"effect.name\">\n </div>\n <span class=\"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors\">\n {{ effect.name }}\n </span>\n </button>\n }\n </div>\n\n <!-- Adjustments -->\n <div class=\"flex flex-col gap-6\">\n @for (adj of adjustments() || []; track adj.id) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium\">{{ adj.name }}</span>\n <ngs-slide-toggle [checked]=\"selectedLayer()?.[adj.id + 'Enabled']\"\n (change)=\"toggleAdjustment(adj.id, $event.checked)\"/>\n </div>\n\n @if (selectedLayer()?.[adj.id + 'Enabled']) {\n <div class=\"flex flex-col gap-4\">\n @if (adj.isGroup) {\n @for (control of adj.controls || []; track control.id) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-muted-foreground\">{{ control.name }}</span>\n @if (control.type === 'color') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.[control.id] || control.default\"\n [ngsColorPickerTriggerFor]=\"colorPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #colorPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.[control.id] || control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\"/>\n </ng-template>\n } @else {\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[control.id] ?? control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\">\n }\n </div>\n @if (control.type !== 'color') {\n <ngs-slider [min]=\"control.min\"\n [max]=\"control.max\"\n [step]=\"control.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[control.id] ?? control.default\"\n (valueChange)=\"updateAdjustment(control.id, $event)\">\n </ngs-slider>\n }\n </div>\n }\n } @else {\n <div class=\"flex items-center gap-4\">\n @if (adj.id === 'border') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n [ngsColorPickerTriggerFor]=\"borderPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #borderPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n (ngModelChange)=\"updateAdjustment('borderColor', $event)\"/>\n </ng-template>\n }\n <ngs-slider class=\"flex-1\"\n [min]=\"adj.min\"\n [max]=\"adj.max\"\n [step]=\"adj.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (valueChange)=\"updateAdjustment(adj.id, $event)\">\n </ngs-slider>\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (ngModelChange)=\"updateAdjustment(adj.id, $event)\">\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;height:100%}:host .scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}:host .scrollbar-hide::-webkit-scrollbar{display:none}:host ngs-slider{--ngs-slider-track-height: 4px;--ngs-slider-thumb-size: 16px}:host input[type=number]::-webkit-inner-spin-button,:host input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ngs-color-picker-thumbnail{width:32px;height:32px;min-width:32px;background:var(--ngs-color-picker-thumbnail-bg, #000)}\n"] }]
|
|
3203
|
+
}] });
|
|
3204
|
+
|
|
3205
|
+
class ImageDesigner {
|
|
3206
|
+
http = inject(HttpClient);
|
|
3207
|
+
destroyRef = inject(DestroyRef);
|
|
3208
|
+
designerService = inject(ImageDesignerService);
|
|
3209
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
3210
|
+
canvasContainer = viewChild('canvasContainer', ...(ngDevMode ? [{ debugName: "canvasContainer" }] : /* istanbul ignore next */ []));
|
|
3211
|
+
title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
3212
|
+
imageSize = input({
|
|
3213
|
+
width: 700,
|
|
3214
|
+
height: 400
|
|
3215
|
+
}, ...(ngDevMode ? [{ debugName: "imageSize" }] : /* istanbul ignore next */ []));
|
|
3216
|
+
defaultFont = input('Inter', ...(ngDevMode ? [{ debugName: "defaultFont" }] : /* istanbul ignore next */ []));
|
|
3217
|
+
scale = input(1, ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
|
|
3218
|
+
minScale = input(0.1, ...(ngDevMode ? [{ debugName: "minScale" }] : /* istanbul ignore next */ []));
|
|
3219
|
+
maxScale = input(3, ...(ngDevMode ? [{ debugName: "maxScale" }] : /* istanbul ignore next */ []));
|
|
3220
|
+
showGuidelines = input(true, ...(ngDevMode ? [{ debugName: "showGuidelines" }] : /* istanbul ignore next */ []));
|
|
3221
|
+
snapToShapes = input(true, ...(ngDevMode ? [{ debugName: "snapToShapes" }] : /* istanbul ignore next */ []));
|
|
3222
|
+
snapToStageCenter = input(true, ...(ngDevMode ? [{ debugName: "snapToStageCenter" }] : /* istanbul ignore next */ []));
|
|
3223
|
+
snapToStageBorders = input(true, ...(ngDevMode ? [{ debugName: "snapToStageBorders" }] : /* istanbul ignore next */ []));
|
|
3224
|
+
guidelineColor = input('blue', ...(ngDevMode ? [{ debugName: "guidelineColor" }] : /* istanbul ignore next */ []));
|
|
3225
|
+
snapRange = input(5, ...(ngDevMode ? [{ debugName: "snapRange" }] : /* istanbul ignore next */ []));
|
|
3226
|
+
showDownloadButton = input(true, { ...(ngDevMode ? { debugName: "showDownloadButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
3227
|
+
uploadFn = input(...(ngDevMode ? [undefined, { debugName: "uploadFn" }] : /* istanbul ignore next */ []));
|
|
3228
|
+
snapshot = input(...(ngDevMode ? [undefined, { debugName: "snapshot" }] : /* istanbul ignore next */ []));
|
|
3229
|
+
snapshotChanged = output();
|
|
3230
|
+
photosDataSource = input(...(ngDevMode ? [undefined, { debugName: "photosDataSource" }] : /* istanbul ignore next */ []));
|
|
3231
|
+
assetsDataSource = input(...(ngDevMode ? [undefined, { debugName: "assetsDataSource" }] : /* istanbul ignore next */ []));
|
|
3232
|
+
historyLimit = input(50, { ...(ngDevMode ? { debugName: "historyLimit" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
3233
|
+
activeItemId = signal('text', ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
|
|
3234
|
+
elements = signal(SVG_ELEMENTS, ...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
|
|
3235
|
+
photos = signal([], ...(ngDevMode ? [{ debugName: "photos" }] : /* istanbul ignore next */ []));
|
|
3236
|
+
assets = signal([], ...(ngDevMode ? [{ debugName: "assets" }] : /* istanbul ignore next */ []));
|
|
3237
|
+
photosFilter = signal('', ...(ngDevMode ? [{ debugName: "photosFilter" }] : /* istanbul ignore next */ []));
|
|
3238
|
+
assetsFilter = signal('', ...(ngDevMode ? [{ debugName: "assetsFilter" }] : /* istanbul ignore next */ []));
|
|
3239
|
+
uploadedImages = signal([], ...(ngDevMode ? [{ debugName: "uploadedImages" }] : /* istanbul ignore next */ []));
|
|
3240
|
+
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
|
|
3241
|
+
uploadPreview = signal(null, ...(ngDevMode ? [{ debugName: "uploadPreview" }] : /* istanbul ignore next */ []));
|
|
3242
|
+
photosPage = signal(1, ...(ngDevMode ? [{ debugName: "photosPage" }] : /* istanbul ignore next */ []));
|
|
3243
|
+
assetsPage = signal(1, ...(ngDevMode ? [{ debugName: "assetsPage" }] : /* istanbul ignore next */ []));
|
|
3244
|
+
isLoadingPhotos = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingPhotos" }] : /* istanbul ignore next */ []));
|
|
3245
|
+
isLoadingAssets = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingAssets" }] : /* istanbul ignore next */ []));
|
|
3246
|
+
hasMoreAssets = signal(true, ...(ngDevMode ? [{ debugName: "hasMoreAssets" }] : /* istanbul ignore next */ []));
|
|
3247
|
+
hasMorePhotos = signal(true, ...(ngDevMode ? [{ debugName: "hasMorePhotos" }] : /* istanbul ignore next */ []));
|
|
3248
|
+
resizeWidth = signal(0, ...(ngDevMode ? [{ debugName: "resizeWidth" }] : /* istanbul ignore next */ []));
|
|
3249
|
+
resizeHeight = signal(0, ...(ngDevMode ? [{ debugName: "resizeHeight" }] : /* istanbul ignore next */ []));
|
|
3250
|
+
resizeUnit = signal('px', ...(ngDevMode ? [{ debugName: "resizeUnit" }] : /* istanbul ignore next */ []));
|
|
3251
|
+
presetCategories = signal(PRESET_CATEGORIES, ...(ngDevMode ? [{ debugName: "presetCategories" }] : /* istanbul ignore next */ []));
|
|
3252
|
+
presetColors = signal([
|
|
3253
|
+
// Basic & Neutral
|
|
3254
|
+
'#000000', '#7a7a7a', '#ffffff',
|
|
3255
|
+
// Reds & Pinks
|
|
3256
|
+
'#f44336', '#d32f2f', '#ff5252',
|
|
3257
|
+
'#e91e63', '#c2185b', '#ff4081',
|
|
3258
|
+
// Purples & Deep Purples
|
|
3259
|
+
'#9c27b0', '#7b1fa2', '#e040fb',
|
|
3260
|
+
'#673ab7', '#512da8', '#7c4dff',
|
|
3261
|
+
// Blues
|
|
3262
|
+
'#3f51b5', '#303f9f', '#536dfe',
|
|
3263
|
+
'#2196f3', '#1976d2', '#448aff',
|
|
3264
|
+
'#03a9f4', '#0288d1', '#40c4ff',
|
|
3265
|
+
// Teals & Cyans
|
|
3266
|
+
'#00bcd4', '#0097a7', '#18ffff',
|
|
3267
|
+
'#009688', '#00796b', '#64ffda',
|
|
3268
|
+
// Greens
|
|
3269
|
+
'#4caf50', '#388e3c', '#00c853',
|
|
3270
|
+
'#8bc34a', '#689f38', '#b2ff59',
|
|
3271
|
+
'#cddc39', '#afb42b', '#eeff41',
|
|
3272
|
+
// Yellows & Oranges
|
|
3273
|
+
'#ffeb3b', '#fbc02d', '#ffff00',
|
|
3274
|
+
'#ffc107', '#ffa000', '#ffc400',
|
|
3275
|
+
'#ff9800', '#f57c00', '#ff9100',
|
|
3276
|
+
'#ff5722', '#e64a19', '#ff3d00',
|
|
3277
|
+
// Browns
|
|
3278
|
+
'#795548', '#5d4037', '#8d6e63',
|
|
3279
|
+
// Blue Grays & Grays
|
|
3280
|
+
'#607d8b', '#455a64', '#78909c',
|
|
3281
|
+
], ...(ngDevMode ? [{ debugName: "presetColors" }] : /* istanbul ignore next */ []));
|
|
3282
|
+
background = signal([], ...(ngDevMode ? [{ debugName: "background" }] : /* istanbul ignore next */ []));
|
|
3283
|
+
resize = signal([], ...(ngDevMode ? [{ debugName: "resize" }] : /* istanbul ignore next */ []));
|
|
3284
|
+
displayScale = this.designerService.scale;
|
|
3285
|
+
zoomPercentage = computed(() => (this.displayScale() * 100).toFixed(0), ...(ngDevMode ? [{ debugName: "zoomPercentage" }] : /* istanbul ignore next */ []));
|
|
3286
|
+
layersFromService = this.designerService.layers;
|
|
3287
|
+
selectedLayerId = this.designerService.selectedLayerId;
|
|
3288
|
+
presetPatterns = signal([
|
|
3289
|
+
...SVG_PATTERNS.map(svg => `data:image/svg+xml;base64,${btoa(svg)}`),
|
|
3290
|
+
], ...(ngDevMode ? [{ debugName: "presetPatterns" }] : /* istanbul ignore next */ []));
|
|
3291
|
+
presetGradients = computed(() => {
|
|
3292
|
+
const width = this.resizeWidth();
|
|
3293
|
+
const height = this.resizeHeight();
|
|
3294
|
+
const isLandscape = width >= height;
|
|
3295
|
+
const baseColors = [
|
|
3296
|
+
// --- VIBRANT & ENERGETIC ---
|
|
3297
|
+
['#ff9a9e', '#fecfef'], ['#f093fb', '#f5576c'], ['#4facfe', '#00f2fe'],
|
|
3298
|
+
['#43e97b', '#38f8d4'], ['#fa709a', '#fee140'], ['#30cfd0', '#330867'],
|
|
3299
|
+
['#ff0844', '#ffb199'], ['#ff8177', '#ff867a'], ['#f83600', '#f9d423'],
|
|
3300
|
+
['#b721ff', '#21d4fd'], ['#00dbde', '#fc00ff'], ['#8ec5fc', '#e0c3fc'],
|
|
3301
|
+
['#6a11cb', '#2575fc'], ['#09203f', '#537895'], ['#00c6fb', '#005bea'],
|
|
3302
|
+
['#ffecd2', '#fcb69f'], ['#ff758c', '#ff7eb3'], ['#868f96', '#596164'],
|
|
3303
|
+
['#0ba360', '#3cba92'], ['#13547a', '#80d0c7'], ['#6a85b6', '#bac8e0'],
|
|
3304
|
+
['#434343', '#000000'], ['#92fe9d', '#00c9ff'], ['#f40076', '#df98fa'],
|
|
3305
|
+
['#f067b4', '#81ffef'], ['#ff4b1f', '#ff9068'], ['#16a085', '#f4d03f'],
|
|
3306
|
+
['#00d2ff', '#3a7bd5'], ['#f7971e', '#ffd200'], ['#f12711', '#f5af19'],
|
|
3307
|
+
['#12c2e9', '#c471ed'], ['#f64f59', '#12c2e9'], ['#74ebd5', '#acb6e5'],
|
|
3308
|
+
['#ff9966', '#ff5e62'], ['#00b09b', '#96c93d'], ['#654ea3', '#eaafc8'],
|
|
3309
|
+
['#4e54c8', '#8f94fb'], ['#ff0099', '#493240'], ['#8e2de2', '#4a00e0'],
|
|
3310
|
+
['#3a1c71', '#d76d77'], ['#1f4037', '#99f2c8'], ['#56ab2f', '#a8e063'],
|
|
3311
|
+
['#f11712', '#0099f7'], ['#11998e', '#38ef7d'], ['#fc466b', '#3f5efb'],
|
|
3312
|
+
['#c94b4b', '#4b134f'], ['#00b4db', '#0083b0'], ['#7b4397', '#dc2430'],
|
|
3313
|
+
['#1e3c72', '#2a5298'], ['#2c3e50', '#fd746c'], ['#f00000', '#dc281e'],
|
|
3314
|
+
['#ff512f', '#dd2476'], ['#ff5f6d', '#ffc371'], ['#114357', '#f29492'],
|
|
3315
|
+
['#40e0d0', '#ff8c00'], ['#ff0080', '#ff8c00'], ['#000046', '#1cb5e0'],
|
|
3316
|
+
['#642b73', '#c6426e'], ['#cb2d3e', '#ef473a'], ['#5614b0', '#dbd65d'],
|
|
3317
|
+
['#00c3ff', '#ffff1c'], ['#1d2b64', '#f8cdda'], ['#1a2a6c', '#b21f1f'],
|
|
3318
|
+
['#cc2b5e', '#753a88'], ['#2193b0', '#6dd5ed'], ['#ee9ca7', '#ffdde1'],
|
|
3319
|
+
['#e65100', '#fdb813'], ['#360033', '#0b8793'], ['#141e30', '#243b55'],
|
|
3320
|
+
['#2c3e50', '#4ca1af'], ['#ff512f', '#f06292'], ['#70e1f5', '#ffd194'],
|
|
3321
|
+
['#1c92d2', '#f2fcfe'], ['#3ca55c', '#b5ac49'], ['#4b6cb7', '#182848'],
|
|
3322
|
+
['#00bf8f', '#001510'], ['#517fa4', '#243949'], ['#1e130c', '#9a8478'],
|
|
3323
|
+
['#000000', '#533440'], ['#304352', '#d7d2cc'], ['#83a4d4', '#b6fbff'],
|
|
3324
|
+
['#4568dc', '#b06ab3'], ['#ef3b36', '#ffffff'], ['#000000', '#434343'],
|
|
3325
|
+
['#0f2027', '#203a43'], ['#373b44', '#4286f4'], ['#8e9eab', '#eef2f3']
|
|
3326
|
+
];
|
|
3327
|
+
return baseColors.map(([c1, c2]) => ({
|
|
3328
|
+
x0: 0,
|
|
3329
|
+
y0: 0,
|
|
3330
|
+
x1: isLandscape ? width : 0,
|
|
3331
|
+
y1: isLandscape ? 0 : height,
|
|
3332
|
+
colorStops: [0, c1, 1, c2],
|
|
3333
|
+
css: `linear-gradient(${isLandscape ? '90deg' : '180deg'}, ${c1}, ${c2})`
|
|
3334
|
+
}));
|
|
3335
|
+
}, ...(ngDevMode ? [{ debugName: "presetGradients" }] : /* istanbul ignore next */ []));
|
|
3336
|
+
settingsPortal = signal(null, ...(ngDevMode ? [{ debugName: "settingsPortal" }] : /* istanbul ignore next */ []));
|
|
3337
|
+
effectsPortal = signal(null, ...(ngDevMode ? [{ debugName: "effectsPortal" }] : /* istanbul ignore next */ []));
|
|
3338
|
+
openEffects() {
|
|
3339
|
+
this.effectsPortal.set(new ComponentPortal(Effects));
|
|
3340
|
+
}
|
|
3341
|
+
constructor() {
|
|
3342
|
+
this.resizeWidth.set(this.imageSize().width);
|
|
3343
|
+
this.resizeHeight.set(this.imageSize().height);
|
|
3344
|
+
effect(() => {
|
|
3345
|
+
const size = this.imageSize();
|
|
3346
|
+
untracked(() => {
|
|
3347
|
+
this.resizeWidth.set(size.width);
|
|
3348
|
+
this.resizeHeight.set(size.height);
|
|
3349
|
+
});
|
|
3350
|
+
});
|
|
3351
|
+
this.designerService.change$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
|
3352
|
+
this.snapshotChanged.emit(this.designerService.getSnapshot());
|
|
3353
|
+
});
|
|
3354
|
+
effect(() => {
|
|
3355
|
+
this.designerService.setScale(this.scale());
|
|
3356
|
+
});
|
|
3357
|
+
effect(() => {
|
|
3358
|
+
this.designerService.setMinMaxScale(this.minScale(), this.maxScale());
|
|
3359
|
+
});
|
|
3360
|
+
effect(() => {
|
|
3361
|
+
this.designerService.updateSnapSettings({
|
|
3362
|
+
showGuidelines: this.showGuidelines(),
|
|
3363
|
+
snapToShapes: this.snapToShapes(),
|
|
3364
|
+
snapToStageCenter: this.snapToStageCenter(),
|
|
3365
|
+
snapToStageBorders: this.snapToStageBorders(),
|
|
3366
|
+
guidelineColor: this.guidelineColor(),
|
|
3367
|
+
snapRange: this.snapRange()
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
effect(() => {
|
|
3371
|
+
const selectedId = this.selectedLayerId();
|
|
3372
|
+
if (selectedId) {
|
|
3373
|
+
this.settingsPortal.set(new ComponentPortal(Settings));
|
|
3374
|
+
}
|
|
3375
|
+
else {
|
|
3376
|
+
this.settingsPortal.set(null);
|
|
3377
|
+
this.effectsPortal.set(null);
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
effect(() => {
|
|
3381
|
+
if (this.activeItemId() === 'photos' && this.photos().length === 0 && !this.isLoadingPhotos() && this.hasMorePhotos()) {
|
|
3382
|
+
this.loadPhotos();
|
|
3383
|
+
}
|
|
3384
|
+
});
|
|
3385
|
+
effect(() => {
|
|
3386
|
+
if ((this.activeItemId() === 'assets' || this.activeItemId() === 'upload') && this.assets().length === 0 && !this.isLoadingAssets() && this.hasMoreAssets()) {
|
|
3387
|
+
this.loadAssets();
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
effect(() => {
|
|
3391
|
+
this.photosFilter();
|
|
3392
|
+
untracked(() => {
|
|
3393
|
+
if (this.activeItemId() === 'photos') {
|
|
3394
|
+
this.photos.set([]);
|
|
3395
|
+
this.photosPage.set(1);
|
|
3396
|
+
this.hasMorePhotos.set(true);
|
|
3397
|
+
this.loadPhotos();
|
|
3398
|
+
}
|
|
3399
|
+
});
|
|
3400
|
+
});
|
|
3401
|
+
effect(() => {
|
|
3402
|
+
this.assetsFilter();
|
|
3403
|
+
untracked(() => {
|
|
3404
|
+
if (this.activeItemId() === 'assets' || this.activeItemId() === 'upload') {
|
|
3405
|
+
this.assets.set([]);
|
|
3406
|
+
this.assetsPage.set(1);
|
|
3407
|
+
this.hasMoreAssets.set(true);
|
|
3408
|
+
this.loadAssets();
|
|
3409
|
+
}
|
|
3410
|
+
});
|
|
3411
|
+
});
|
|
3412
|
+
effect(() => {
|
|
3413
|
+
const snapshot = this.snapshot();
|
|
3414
|
+
if (snapshot && this.designerService.isInitialized()) {
|
|
3415
|
+
const nextVersion = snapshot.version ?? 0;
|
|
3416
|
+
const currentVersion = this.designerService.currentSnapshotVersion;
|
|
3417
|
+
if (nextVersion !== currentVersion) {
|
|
3418
|
+
console.log(`Snapshot version: ${nextVersion}, Designer version: ${currentVersion}`);
|
|
3419
|
+
console.log(`Snapshot version changed, loading new snapshot`);
|
|
3420
|
+
this.designerService.loadSnapshot(snapshot, true);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
ngOnInit() {
|
|
3426
|
+
this.designerService.historyLimit = this.historyLimit();
|
|
3427
|
+
if (this.assets().length > 0) {
|
|
3428
|
+
this.uploadedImages.set([...this.assets()]);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
onFileSelected(event) {
|
|
3432
|
+
if (event.files.length > 0) {
|
|
3433
|
+
const file = event.files[0];
|
|
3434
|
+
const uploadFn = this.uploadFn();
|
|
3435
|
+
if (uploadFn) {
|
|
3436
|
+
const result = uploadFn(file);
|
|
3437
|
+
this.isUploading.set(true);
|
|
3438
|
+
const handleSuccess = async (result) => {
|
|
3439
|
+
let photo;
|
|
3440
|
+
if (typeof result === 'string') {
|
|
3441
|
+
const dimensions = await this.getImageDimensions(result);
|
|
3442
|
+
photo = {
|
|
3443
|
+
id: Math.random().toString(36).substring(7),
|
|
3444
|
+
url: result,
|
|
3445
|
+
width: dimensions.width,
|
|
3446
|
+
height: dimensions.height
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
else {
|
|
3450
|
+
photo = result;
|
|
3451
|
+
}
|
|
3452
|
+
this.uploadedImages.update(images => [photo, ...images]);
|
|
3453
|
+
this.isUploading.set(false);
|
|
3454
|
+
};
|
|
3455
|
+
const handleError = () => {
|
|
3456
|
+
this.isUploading.set(false);
|
|
3457
|
+
};
|
|
3458
|
+
if (isObservable(result)) {
|
|
3459
|
+
result.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3460
|
+
next: handleSuccess,
|
|
3461
|
+
error: handleError
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
else if (result instanceof Promise) {
|
|
3465
|
+
result.then(handleSuccess).catch(handleError);
|
|
3466
|
+
}
|
|
3467
|
+
else {
|
|
3468
|
+
handleSuccess(result);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
else {
|
|
3472
|
+
this.fakeUpload(file);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
fakeUpload(file) {
|
|
3477
|
+
const reader = new FileReader();
|
|
3478
|
+
reader.onload = async (e) => {
|
|
3479
|
+
const url = e.target.result;
|
|
3480
|
+
const dimensions = await this.getImageDimensions(url);
|
|
3481
|
+
const photo = {
|
|
3482
|
+
id: Math.random().toString(36).substring(7),
|
|
3483
|
+
name: file.name,
|
|
3484
|
+
url: url,
|
|
3485
|
+
width: dimensions.width,
|
|
3486
|
+
height: dimensions.height
|
|
3487
|
+
};
|
|
3488
|
+
this.uploadedImages.update(images => [photo, ...images]);
|
|
3489
|
+
};
|
|
3490
|
+
reader.readAsDataURL(file);
|
|
3491
|
+
}
|
|
3492
|
+
getImageDimensions(url) {
|
|
3493
|
+
return new Promise(resolve => {
|
|
3494
|
+
const img = new Image();
|
|
3495
|
+
img.crossOrigin = 'anonymous';
|
|
3496
|
+
img.onload = () => {
|
|
3497
|
+
resolve({ width: img.width, height: img.height });
|
|
3498
|
+
};
|
|
3499
|
+
img.onerror = () => {
|
|
3500
|
+
resolve({ width: 0, height: 0 });
|
|
3501
|
+
};
|
|
3502
|
+
img.src = url;
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
async addUploadedImage(photo, x = 0, y = 0) {
|
|
3506
|
+
const canvasWidth = this.resizeWidth();
|
|
3507
|
+
const canvasHeight = this.resizeHeight();
|
|
3508
|
+
const url = photo.url;
|
|
3509
|
+
let width = canvasWidth;
|
|
3510
|
+
let height = canvasWidth * (photo.height / photo.width);
|
|
3511
|
+
if (height > canvasHeight) {
|
|
3512
|
+
height = canvasHeight;
|
|
3513
|
+
width = height * (photo.width / photo.height);
|
|
3514
|
+
}
|
|
3515
|
+
const id = await this.designerService.addLayer({
|
|
3516
|
+
type: 'image',
|
|
3517
|
+
src: url,
|
|
3518
|
+
name: 'Uploaded Image',
|
|
3519
|
+
width,
|
|
3520
|
+
height,
|
|
3521
|
+
x,
|
|
3522
|
+
y
|
|
3523
|
+
});
|
|
3524
|
+
if (id) {
|
|
3525
|
+
this.selectLayer(id);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
loadAssets() {
|
|
3529
|
+
const dataSource = this.assetsDataSource();
|
|
3530
|
+
if (!dataSource || this.isLoadingAssets() || !this.hasMoreAssets()) {
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
this.isLoadingAssets.set(true);
|
|
3534
|
+
const pageSize = 30;
|
|
3535
|
+
const page = this.assetsPage();
|
|
3536
|
+
const startRow = (page - 1) * pageSize;
|
|
3537
|
+
const endRow = page * pageSize;
|
|
3538
|
+
dataSource.getItems({
|
|
3539
|
+
startRow,
|
|
3540
|
+
endRow,
|
|
3541
|
+
page,
|
|
3542
|
+
pageSize,
|
|
3543
|
+
filterModel: this.assetsFilter(),
|
|
3544
|
+
successCallback: (data, lastRow) => {
|
|
3545
|
+
this.assets.update(assets => [...assets, ...data]);
|
|
3546
|
+
if (data.length === 0) {
|
|
3547
|
+
this.hasMoreAssets.set(false);
|
|
3548
|
+
}
|
|
3549
|
+
this.isLoadingAssets.set(false);
|
|
3550
|
+
},
|
|
3551
|
+
failCallback: () => {
|
|
3552
|
+
this.isLoadingAssets.set(false);
|
|
3553
|
+
}
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
onAssetsScroll(event) {
|
|
3557
|
+
const element = event.target;
|
|
3558
|
+
if (element.scrollHeight - element.scrollTop <= element.clientHeight + 50) {
|
|
3559
|
+
if (!this.isLoadingAssets() && this.hasMoreAssets()) {
|
|
3560
|
+
this.assetsPage.update(p => p + 1);
|
|
3561
|
+
this.loadAssets();
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
loadPhotos() {
|
|
3566
|
+
if (this.isLoadingPhotos() || !this.hasMorePhotos())
|
|
3567
|
+
return;
|
|
3568
|
+
this.isLoadingPhotos.set(true);
|
|
3569
|
+
const ds = this.photosDataSource() || this.defaultPhotosDataSource;
|
|
3570
|
+
const page = this.photosPage();
|
|
3571
|
+
const pageSize = 30;
|
|
3572
|
+
ds.getItems({
|
|
3573
|
+
startRow: (page - 1) * pageSize,
|
|
3574
|
+
endRow: page * pageSize,
|
|
3575
|
+
page: page,
|
|
3576
|
+
pageSize: pageSize,
|
|
3577
|
+
filterModel: this.photosFilter(),
|
|
3578
|
+
successCallback: (data, lastRow) => {
|
|
3579
|
+
this.photos.update(photos => [...photos, ...data]);
|
|
3580
|
+
if (data.length === 0) {
|
|
3581
|
+
this.hasMorePhotos.set(false);
|
|
3582
|
+
}
|
|
3583
|
+
this.isLoadingPhotos.set(false);
|
|
3584
|
+
},
|
|
3585
|
+
failCallback: () => {
|
|
3586
|
+
this.isLoadingPhotos.set(false);
|
|
3587
|
+
}
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
defaultPhotosDataSource = createDefaultPhotosDataSource(this.http);
|
|
3591
|
+
onPhotosScroll(event) {
|
|
3592
|
+
const target = event.target;
|
|
3593
|
+
if (target && target.scrollHeight - target.scrollTop <= target.clientHeight + 100) {
|
|
3594
|
+
if (!this.isLoadingPhotos() && this.hasMorePhotos()) {
|
|
3595
|
+
this.photosPage.update(p => p + 1);
|
|
3596
|
+
this.loadPhotos();
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
async addImage(photo, x = 0, y = 0) {
|
|
3601
|
+
const canvasWidth = this.resizeWidth();
|
|
3602
|
+
const canvasHeight = this.resizeHeight();
|
|
3603
|
+
let width = canvasWidth;
|
|
3604
|
+
let height = canvasWidth * (photo.height / photo.width);
|
|
3605
|
+
if (height > canvasHeight) {
|
|
3606
|
+
height = canvasHeight;
|
|
3607
|
+
width = height * (photo.width / photo.height);
|
|
3608
|
+
}
|
|
3609
|
+
const id = await this.designerService.addLayer({
|
|
3610
|
+
type: 'image',
|
|
3611
|
+
src: photo.url,
|
|
3612
|
+
name: photo.name,
|
|
3613
|
+
width,
|
|
3614
|
+
height,
|
|
3615
|
+
x,
|
|
3616
|
+
y
|
|
3617
|
+
});
|
|
3618
|
+
if (id) {
|
|
3619
|
+
this.selectLayer(id);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
ngAfterViewInit() {
|
|
3623
|
+
if (!this.isBrowser) {
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
const container = this.canvasContainer()?.nativeElement;
|
|
3627
|
+
if (container) {
|
|
3628
|
+
console.log('Initializing ImageDesignerService in requestAnimationFrame');
|
|
3629
|
+
requestAnimationFrame(() => {
|
|
3630
|
+
if (!this.canvasContainer()) {
|
|
3631
|
+
console.warn('Canvas container disappeared before initialization');
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
const currentContainer = this.canvasContainer().nativeElement;
|
|
3635
|
+
// Final check before init, if dimensions are 0, wait a bit more
|
|
3636
|
+
if (currentContainer.offsetWidth === 0 || currentContainer.offsetHeight === 0) {
|
|
3637
|
+
console.warn('Container dimensions are still 0, retrying init');
|
|
3638
|
+
setTimeout(() => this.ngAfterViewInit(), 250);
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
console.log('Initializing ImageDesignerService with container:', currentContainer);
|
|
3642
|
+
console.log('Container dimensions in rAF:', currentContainer.offsetWidth, 'x', currentContainer.offsetHeight);
|
|
3643
|
+
this.designerService.init(currentContainer, this.imageSize(), this.defaultFont(), this.scale(), this.minScale(), this.maxScale()).then(() => {
|
|
3644
|
+
console.log('ImageDesignerService initialization completed');
|
|
3645
|
+
});
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
else {
|
|
3649
|
+
console.warn('Canvas container not found during ngAfterViewInit');
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
ngOnDestroy() {
|
|
3653
|
+
this.designerService.destroy();
|
|
3654
|
+
}
|
|
3655
|
+
toggleAside() {
|
|
3656
|
+
this.activeItemId.set(this.activeItemId() ? null : 'text');
|
|
3657
|
+
}
|
|
3658
|
+
zoomIn() {
|
|
3659
|
+
this.designerService.zoomIn();
|
|
3660
|
+
}
|
|
3661
|
+
zoomOut() {
|
|
3662
|
+
this.designerService.zoomOut();
|
|
3663
|
+
}
|
|
3664
|
+
undo() {
|
|
3665
|
+
this.designerService.undo();
|
|
3666
|
+
}
|
|
3667
|
+
redo() {
|
|
3668
|
+
this.designerService.redo();
|
|
3669
|
+
}
|
|
3670
|
+
resetZoom() {
|
|
3671
|
+
this.designerService.setScale(1);
|
|
3672
|
+
}
|
|
3673
|
+
onWheel(event) {
|
|
3674
|
+
if (event.metaKey || event.ctrlKey) {
|
|
3675
|
+
// preventDefault is not allowed during event replay (e.g. during hydration).
|
|
3676
|
+
// EventPhase.REPLAY is 101 in Angular.
|
|
3677
|
+
if (event.eventPhase !== 101) {
|
|
3678
|
+
event.preventDefault();
|
|
3679
|
+
}
|
|
3680
|
+
if (event.deltaY < 0) {
|
|
3681
|
+
this.zoomIn();
|
|
3682
|
+
}
|
|
3683
|
+
else {
|
|
3684
|
+
this.zoomOut();
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
onKeyDown(event) {
|
|
3689
|
+
const activeElement = document.activeElement;
|
|
3690
|
+
const isInput = activeElement instanceof HTMLInputElement ||
|
|
3691
|
+
activeElement instanceof HTMLTextAreaElement ||
|
|
3692
|
+
(activeElement instanceof HTMLElement && activeElement.isContentEditable);
|
|
3693
|
+
if (isInput) {
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
3697
|
+
event.preventDefault();
|
|
3698
|
+
const step = event.shiftKey ? 10 : 1;
|
|
3699
|
+
if (event.key === 'ArrowLeft') {
|
|
3700
|
+
this.designerService.moveSelectedLayers(-step, 0);
|
|
3701
|
+
}
|
|
3702
|
+
else if (event.key === 'ArrowRight') {
|
|
3703
|
+
this.designerService.moveSelectedLayers(step, 0);
|
|
3704
|
+
}
|
|
3705
|
+
else if (event.key === 'ArrowUp') {
|
|
3706
|
+
this.designerService.moveSelectedLayers(0, -step);
|
|
3707
|
+
}
|
|
3708
|
+
else if (event.key === 'ArrowDown') {
|
|
3709
|
+
this.designerService.moveSelectedLayers(0, step);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
3713
|
+
this.designerService.deleteSelectedLayers();
|
|
3714
|
+
}
|
|
3715
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
|
|
3716
|
+
event.preventDefault();
|
|
3717
|
+
if (event.shiftKey) {
|
|
3718
|
+
this.redo();
|
|
3719
|
+
}
|
|
3720
|
+
else {
|
|
3721
|
+
this.undo();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'y') {
|
|
3725
|
+
event.preventDefault();
|
|
3726
|
+
this.redo();
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
selectLayer(id) {
|
|
3730
|
+
this.designerService.selectLayer(id);
|
|
3731
|
+
}
|
|
3732
|
+
toggleLayerVisibility(id, event) {
|
|
3733
|
+
event.stopPropagation();
|
|
3734
|
+
this.designerService.toggleLayerVisibility(id);
|
|
3735
|
+
}
|
|
3736
|
+
toggleLayerLock(id, event) {
|
|
3737
|
+
event.stopPropagation();
|
|
3738
|
+
this.designerService.toggleLayerLock(id);
|
|
3739
|
+
}
|
|
3740
|
+
deleteLayer(id, event) {
|
|
3741
|
+
event.stopPropagation();
|
|
3742
|
+
this.designerService.deleteLayer(id);
|
|
3743
|
+
}
|
|
3744
|
+
onDragOver(event) {
|
|
3745
|
+
if (event.dataTransfer?.types.includes('application/x-ngs-text-type') ||
|
|
3746
|
+
event.dataTransfer?.types.includes('application/x-ngs-image-data')) {
|
|
3747
|
+
event.preventDefault();
|
|
3748
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
onTextDragStart(event, type) {
|
|
3752
|
+
event.dataTransfer?.setData('application/x-ngs-text-type', type);
|
|
3753
|
+
if (event.target instanceof HTMLElement) {
|
|
3754
|
+
const el = event.target;
|
|
3755
|
+
el.classList.add('is-dragging');
|
|
3756
|
+
setTimeout(() => el.classList.remove('is-dragging'), 0);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
onImageDragStart(event, photo) {
|
|
3760
|
+
const data = 'url' in photo
|
|
3761
|
+
? { url: photo.url, name: photo.name, width: photo.width, height: photo.height, type: 'image' }
|
|
3762
|
+
: { url: photo.data, name: photo.name, width: 100, height: 100, type: 'shape' };
|
|
3763
|
+
event.dataTransfer?.setData('application/x-ngs-image-data', JSON.stringify(data));
|
|
3764
|
+
if (event.target instanceof HTMLElement) {
|
|
3765
|
+
const el = event.target;
|
|
3766
|
+
el.classList.add('is-dragging');
|
|
3767
|
+
setTimeout(() => el.classList.remove('is-dragging'), 0);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
onDrop(event) {
|
|
3771
|
+
const textType = event.dataTransfer?.getData('application/x-ngs-text-type');
|
|
3772
|
+
const imageDataStr = event.dataTransfer?.getData('application/x-ngs-image-data');
|
|
3773
|
+
if (!textType && !imageDataStr)
|
|
3774
|
+
return;
|
|
3775
|
+
event.preventDefault();
|
|
3776
|
+
const container = this.canvasContainer()?.nativeElement;
|
|
3777
|
+
if (!container)
|
|
3778
|
+
return;
|
|
3779
|
+
const rect = container.getBoundingClientRect();
|
|
3780
|
+
const scale = this.designerService.scale();
|
|
3781
|
+
// Calculate position relative to the stage center
|
|
3782
|
+
const canvasRect = this.designerService['canvasRect'];
|
|
3783
|
+
if (!canvasRect)
|
|
3784
|
+
return;
|
|
3785
|
+
const centerX = canvasRect.x();
|
|
3786
|
+
const centerY = canvasRect.y();
|
|
3787
|
+
const x = (event.clientX - rect.left - centerX) / scale;
|
|
3788
|
+
const y = (event.clientY - rect.top - centerY) / scale;
|
|
3789
|
+
if (textType) {
|
|
3790
|
+
switch (textType) {
|
|
3791
|
+
case 'header':
|
|
3792
|
+
this.addHeader(x, y);
|
|
3793
|
+
break;
|
|
3794
|
+
case 'subheader':
|
|
3795
|
+
this.addSubheader(x, y);
|
|
3796
|
+
break;
|
|
3797
|
+
case 'body':
|
|
3798
|
+
this.addBodyText(x, y);
|
|
3799
|
+
break;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
else if (imageDataStr) {
|
|
3803
|
+
try {
|
|
3804
|
+
const data = JSON.parse(imageDataStr);
|
|
3805
|
+
if (data.type === 'image') {
|
|
3806
|
+
this.addImage({
|
|
3807
|
+
url: data.url,
|
|
3808
|
+
name: data.name,
|
|
3809
|
+
width: data.width,
|
|
3810
|
+
height: data.height,
|
|
3811
|
+
id: ''
|
|
3812
|
+
}, x, y);
|
|
3813
|
+
}
|
|
3814
|
+
else if (data.type === 'shape') {
|
|
3815
|
+
this.addShape({
|
|
3816
|
+
name: data.name,
|
|
3817
|
+
data: data.url
|
|
3818
|
+
}, x, y);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
catch (e) {
|
|
3822
|
+
console.error('Failed to parse image data', e);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
drop(event) {
|
|
3827
|
+
if (event.previousIndex !== event.currentIndex) {
|
|
3828
|
+
this.designerService.reorderLayers(event.previousIndex, event.currentIndex);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
download() {
|
|
3832
|
+
this.designerService.downloadImage();
|
|
3833
|
+
}
|
|
3834
|
+
getBase64Image() {
|
|
3835
|
+
return this.designerService.getBase64Image();
|
|
3836
|
+
}
|
|
3837
|
+
getLayerIcon(type) {
|
|
3838
|
+
const iconMap = {
|
|
3839
|
+
'text': 'fluent:text-field-24-regular',
|
|
3840
|
+
'image': 'fluent:image-24-regular',
|
|
3841
|
+
'pattern': 'fluent:grid-dots-24-regular',
|
|
3842
|
+
};
|
|
3843
|
+
return iconMap[type || ''] || 'fluent:shape-subtract-24-regular';
|
|
3844
|
+
}
|
|
3845
|
+
selectedFontSize = signal(24, ...(ngDevMode ? [{ debugName: "selectedFontSize" }] : /* istanbul ignore next */ []));
|
|
3846
|
+
selectedFontWeight = signal(400, ...(ngDevMode ? [{ debugName: "selectedFontWeight" }] : /* istanbul ignore next */ []));
|
|
3847
|
+
async addHeader(x = 0, y = 0) {
|
|
3848
|
+
const id = await this.designerService.addLayer({
|
|
3849
|
+
type: 'text',
|
|
3850
|
+
text: 'Add a heading',
|
|
3851
|
+
fontSize: 40,
|
|
3852
|
+
fontWeight: 'bold',
|
|
3853
|
+
fontFamily: 'Inter',
|
|
3854
|
+
name: 'Heading',
|
|
3855
|
+
align: 'center',
|
|
3856
|
+
x,
|
|
3857
|
+
y
|
|
3858
|
+
});
|
|
3859
|
+
if (id) {
|
|
3860
|
+
this.selectLayer(id);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
async addSubheader(x = 0, y = 0) {
|
|
3864
|
+
const id = await this.designerService.addLayer({
|
|
3865
|
+
type: 'text',
|
|
3866
|
+
text: 'Add a subheading',
|
|
3867
|
+
fontSize: 24,
|
|
3868
|
+
fontWeight: '600',
|
|
3869
|
+
fontFamily: 'Inter',
|
|
3870
|
+
name: 'Subheading',
|
|
3871
|
+
align: 'center',
|
|
3872
|
+
x,
|
|
3873
|
+
y
|
|
3874
|
+
});
|
|
3875
|
+
if (id) {
|
|
3876
|
+
this.selectLayer(id);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
async addBodyText(x = 0, y = 0) {
|
|
3880
|
+
const id = await this.designerService.addLayer({
|
|
3881
|
+
type: 'text',
|
|
3882
|
+
text: 'Add body text',
|
|
3883
|
+
fontSize: 16,
|
|
3884
|
+
fontFamily: 'Inter',
|
|
3885
|
+
name: 'Body text',
|
|
3886
|
+
align: 'center',
|
|
3887
|
+
x,
|
|
3888
|
+
y
|
|
3889
|
+
});
|
|
3890
|
+
if (id) {
|
|
3891
|
+
this.selectLayer(id);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
async addShape(shape, x = 0, y = 0) {
|
|
3895
|
+
const id = await this.designerService.addLayer({
|
|
3896
|
+
type: 'shape',
|
|
3897
|
+
name: shape.name,
|
|
3898
|
+
data: shape.data,
|
|
3899
|
+
width: 100,
|
|
3900
|
+
height: 100,
|
|
3901
|
+
fill: '#64748b',
|
|
3902
|
+
x,
|
|
3903
|
+
y
|
|
3904
|
+
});
|
|
3905
|
+
if (id) {
|
|
3906
|
+
this.selectLayer(id);
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
setGradient(gradient) {
|
|
3910
|
+
this.designerService.setCanvasBackground({
|
|
3911
|
+
x0: gradient.x0,
|
|
3912
|
+
y0: gradient.y0,
|
|
3913
|
+
x1: gradient.x1,
|
|
3914
|
+
y1: gradient.y1,
|
|
3915
|
+
colorStops: gradient.colorStops
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
setColor(color) {
|
|
3919
|
+
this.designerService.setCanvasBackground(color);
|
|
3920
|
+
}
|
|
3921
|
+
async addPattern(url) {
|
|
3922
|
+
const id = await this.designerService.addLayer({
|
|
3923
|
+
type: 'pattern',
|
|
3924
|
+
name: 'Pattern',
|
|
3925
|
+
patternImage: url,
|
|
3926
|
+
width: this.resizeWidth(),
|
|
3927
|
+
height: this.resizeHeight(),
|
|
3928
|
+
x: 0,
|
|
3929
|
+
y: 0
|
|
3930
|
+
});
|
|
3931
|
+
if (id) {
|
|
3932
|
+
this.selectLayer(id);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
applyResize() {
|
|
3936
|
+
this.designerService.updateSize(this.canvasContainer().nativeElement, {
|
|
3937
|
+
width: this.resizeWidth(),
|
|
3938
|
+
height: this.resizeHeight()
|
|
3939
|
+
}, true, true);
|
|
3940
|
+
}
|
|
3941
|
+
selectPreset(preset) {
|
|
3942
|
+
this.resizeWidth.set(preset.width);
|
|
3943
|
+
this.resizeHeight.set(preset.height);
|
|
3944
|
+
this.applyResize();
|
|
3945
|
+
}
|
|
3946
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesigner, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3947
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: ImageDesigner, isStandalone: true, selector: "ngs-image-designer", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, imageSize: { classPropertyName: "imageSize", publicName: "imageSize", isSignal: true, isRequired: false, transformFunction: null }, defaultFont: { classPropertyName: "defaultFont", publicName: "defaultFont", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, minScale: { classPropertyName: "minScale", publicName: "minScale", isSignal: true, isRequired: false, transformFunction: null }, maxScale: { classPropertyName: "maxScale", publicName: "maxScale", isSignal: true, isRequired: false, transformFunction: null }, showGuidelines: { classPropertyName: "showGuidelines", publicName: "showGuidelines", isSignal: true, isRequired: false, transformFunction: null }, snapToShapes: { classPropertyName: "snapToShapes", publicName: "snapToShapes", isSignal: true, isRequired: false, transformFunction: null }, snapToStageCenter: { classPropertyName: "snapToStageCenter", publicName: "snapToStageCenter", isSignal: true, isRequired: false, transformFunction: null }, snapToStageBorders: { classPropertyName: "snapToStageBorders", publicName: "snapToStageBorders", isSignal: true, isRequired: false, transformFunction: null }, guidelineColor: { classPropertyName: "guidelineColor", publicName: "guidelineColor", isSignal: true, isRequired: false, transformFunction: null }, snapRange: { classPropertyName: "snapRange", publicName: "snapRange", isSignal: true, isRequired: false, transformFunction: null }, showDownloadButton: { classPropertyName: "showDownloadButton", publicName: "showDownloadButton", isSignal: true, isRequired: false, transformFunction: null }, uploadFn: { classPropertyName: "uploadFn", publicName: "uploadFn", isSignal: true, isRequired: false, transformFunction: null }, snapshot: { classPropertyName: "snapshot", publicName: "snapshot", isSignal: true, isRequired: false, transformFunction: null }, photosDataSource: { classPropertyName: "photosDataSource", publicName: "photosDataSource", isSignal: true, isRequired: false, transformFunction: null }, assetsDataSource: { classPropertyName: "assetsDataSource", publicName: "assetsDataSource", isSignal: true, isRequired: false, transformFunction: null }, historyLimit: { classPropertyName: "historyLimit", publicName: "historyLimit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { snapshotChanged: "snapshotChanged" }, host: { listeners: { "keydown": "onKeyDown($event)" }, classAttribute: "ngs-image-designer" }, providers: [
|
|
3948
|
+
ImageDesignerService,
|
|
3949
|
+
{
|
|
3950
|
+
provide: IMAGE_DESIGNER,
|
|
3951
|
+
useExisting: forwardRef(() => ImageDesigner)
|
|
3952
|
+
}
|
|
3953
|
+
], viewQueries: [{ propertyName: "canvasContainer", first: true, predicate: ["canvasContainer"], descendants: true, isSignal: true }], exportAs: ["ngsImageDesigner"], ngImport: i0, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-sidebar class=\"border-r border-border h-full\">\n <ngs-tab-panel hideContentIfTabNotSelected [activeItemId]=\"activeItemId()\" class=\"h-full\">\n <ngs-tab-panel-content>\n <ngs-tab-panel-nav>\n <ngs-tab-panel-item for=\"text\" (click)=\"activeItemId.set('text')\">\n <ngs-icon name=\"fluent:text-field-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Text</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"photos\" (click)=\"activeItemId.set('photos')\">\n <ngs-icon name=\"fluent:image-copy-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Photos</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"elements\" (click)=\"activeItemId.set('elements')\">\n <ngs-icon name=\"fluent:shape-subtract-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Elements</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"upload\" (click)=\"activeItemId.set('upload')\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Upload</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"background\" (click)=\"activeItemId.set('background')\">\n <ngs-icon name=\"fluent:grid-dots-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Background</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"layers\" (click)=\"activeItemId.set('layers')\">\n <ngs-icon name=\"fluent:layer-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Layers</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"resize\" (click)=\"activeItemId.set('resize')\">\n <ngs-icon name=\"fluent:resize-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Resize</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n </ngs-tab-panel-nav>\n </ngs-tab-panel-content>\n <ngs-tab-panel-aside class=\"border-s border-border\">\n <ng-template ngsTabPanelAsideContent=\"text\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Text</ngs-panel-header>\n <ngs-panel-content class=\"p-4 flex flex-col gap-4\">\n <div class=\"grid grid-cols-1 gap-4 mt-2\">\n <button (click)=\"addHeader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'header')\"\n class=\"w-full text-left p-4 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-3xl font-bold pointer-events-none\">Add a heading</div>\n </button>\n <button (click)=\"addSubheader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'subheader')\"\n class=\"w-full text-left p-3 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-xl font-semibold pointer-events-none\">Add a subheading</div>\n </button>\n <button (click)=\"addBodyText()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'body')\"\n class=\"w-full text-left p-2 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-base pointer-events-none\">Add body text</div>\n </button>\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"photos\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n<!-- <ngs-form-field class=\"w-full\" subscriptHiddenIfEmpty>-->\n<!-- <ngs-icon name=\"fluent:search-24-regular\" ngsIconPrefix/>-->\n<!-- <input type=\"text\"-->\n<!-- ngsInput-->\n<!-- placeholder=\"Search photos\"-->\n<!-- [ngModel]=\"photosFilter()\"-->\n<!-- (ngModelChange)=\"photosFilter.set($event)\"/>-->\n<!-- </ngs-form-field>-->\n Photos\n </ngs-panel-header>\n <ngs-panel-content class=\"relative flex-1 p-0 overflow-hidden\">\n <ngs-scrollbar-area (scrolled)=\"onPhotosScroll($event)\" class=\"h-full\">\n <div class=\"p-4\">\n <div class=\"masonry-grid\">\n @for (photo of photos(); track photo.id) {\n <button (click)=\"addImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2 hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.thumbUrl || photo.url\"\n [alt]=\"photo.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingPhotos()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n @if (photos().length === 0 && !isLoadingPhotos()) {\n <div class=\"text-center p-4 text-sm text-neutral-500\">\n No photos found\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"elements\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Elements</ngs-panel-header>\n <ngs-panel-content class=\"p-4 overflow-y-auto\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (element of elements(); track element.name) {\n <button (click)=\"addShape(element)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, element)\"\n class=\"aspect-square p-2 border border-border rounded-lg hover:bg-surface-container\n transition-colors flex items-center justify-center\">\n <img [src]=\"element.data\" [alt]=\"element.name\"\n class=\"max-w-full max-h-full object-contain pointer-events-none\"/>\n </button>\n }\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"upload\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n <ngs-toolbar>\n <div>Upload</div>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"filled\"\n ngsUploadTrigger\n [accept]=\"'image/*'\"\n (fileSelected)=\"onFileSelected($event)\"\n [loading]=\"isUploading()\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\"/>\n Add Image\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content>\n @if (uploadedImages().length > 0 || assets().length > 0) {\n <ngs-scrollbar-area (scrolled)=\"onAssetsScroll($event)\">\n <div class=\"p-4 flex flex-col gap-4 min-h-full\">\n @if (isUploading() || uploadPreview()) {\n <div class=\"relative aspect-video border border-border rounded-lg overflow-hidden bg-surface-container\">\n @if (uploadPreview()) {\n <img [src]=\"uploadPreview()\" class=\"w-full h-full object-contain\" alt=\"Preview\"/>\n }\n @if (isUploading()) {\n <div class=\"absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[2px]\">\n <div class=\"flex flex-col items-center gap-2\">\n <ngs-progress-spinner diameter=\"32\" color=\"white\"/>\n <span class=\"text-xs font-medium text-white\">Uploading...</span>\n </div>\n </div>\n }\n </div>\n }\n\n @if (uploadedImages().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Your Uploads</div>\n <div class=\"masonry-grid\">\n @for (photo of uploadedImages(); track $index) {\n <button (click)=\"addUploadedImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.url\" class=\"w-full h-auto block pointer-events-none\" alt=\"Uploaded image\"/>\n </button>\n }\n </div>\n </div>\n }\n\n @if (assets().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Assets</div>\n <div class=\"masonry-grid\">\n @for (asset of assets(); track asset.id) {\n <button (click)=\"addImage(asset)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, asset)\"\n [style.aspect-ratio]=\"asset.width + '/' + asset.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"asset.thumbUrl || asset.url\"\n [alt]=\"asset.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingAssets()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n }\n\n @if (uploadedImages().length === 0 && assets().length === 0 && !isLoadingAssets()) {\n <div class=\"h-full flex items-center justify-center\">\n <div class=\"text-sm text-neutral-500\">No images uploaded yet.</div>\n </div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"background\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Background</ngs-panel-header>\n <ngs-panel-content>\n <ngs-tab-group animationDuration=\"0\" class=\"h-full\">\n <ngs-tab label=\"Color\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <ngs-accordion [multi]=\"false\">\n <ngs-expansion-panel [expanded]=\"true\">\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Colors</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"py-2\">\n <ngs-color-switcher [colors]=\"presetColors()\" (colorChange)=\"setColor($event)\"/>\n </div>\n </ngs-expansion-panel>\n <ngs-expansion-panel>\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Gradients</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"grid grid-cols-3 gap-3 py-2\">\n @for (gradient of presetGradients(); track $index) {\n <button (click)=\"setGradient(gradient)\"\n [style.background]=\"gradient.css\"\n class=\"aspect-[16/6.75] w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all\">\n </button>\n }\n </div>\n </ngs-expansion-panel>\n </ngs-accordion>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n <ngs-tab label=\"Patterns\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (pattern of presetPatterns(); track $index) {\n <button (click)=\"addPattern(pattern)\"\n class=\"aspect-square w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all overflow-hidden\">\n <img [src]=\"pattern\" class=\"w-full h-full object-cover\" alt=\"pattern\">\n </button>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"layers\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Layers</ngs-panel-header>\n <ngs-panel-content>\n @if (layersFromService().length > 0) {\n <ngs-list cdkDropList (cdkDropListDropped)=\"drop($event)\">\n @for (layer of layersFromService(); track layer.id) {\n <ngs-list-item cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragStartDelay]=\"100\"\n (click)=\"selectLayer(layer.id!)\"\n [class.is-active]=\"selectedLayerId() === layer.id\"\n class=\"relative group bg-surface\">\n <ngs-icon [name]=\"getLayerIcon(layer.type)\" ngsListItemIcon/>\n <div ngsListItemLine>\n {{ layer.name || layer.type }}\n <!-- {{ layer.text || '' }}-->\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 flex items-center\">\n <button ngsIconButton (click)=\"toggleLayerVisibility(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.visible === false ? 'fluent:eye-off-24-regular' : 'fluent:eye-24-regular'\"/>\n </button>\n <button ngsIconButton (click)=\"toggleLayerLock(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.locked ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"\n class=\"size-5\"/>\n </button>\n <button ngsIconButton (click)=\"deleteLayer(layer.id!, $event)\" [disabled]=\"layer.locked\">\n <ngs-icon name=\"fluent:delete-24-regular\" class=\"size-5\"/>\n </button>\n </div>\n\n <div *cdkDragPlaceholder\n class=\"min-h-[var(--ngs-list-item-min-height)] bg-surface-container\"></div>\n </ngs-list-item>\n }\n </ngs-list>\n } @else {\n <div class=\"text-sm p-4 h-full flex items-center justify-center\">No layers available</div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"resize\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Resize</ngs-panel-header>\n <ngs-panel-content>\n <ngs-scrollbar-area>\n <div class=\"p-4 flex flex-col gap-4\">\n <div class=\"flex flex-col gap-4\">\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Width (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeWidth\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Height (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeHeight\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Units</ngs-label>\n <ngs-select [(ngModel)]=\"resizeUnit\">\n <ngs-option value=\"px\">px</ngs-option>\n </ngs-select>\n </ngs-form-field>\n\n <button ngsButton=\"filled\" fullWidth (click)=\"applyResize()\">Resize</button>\n </div>\n\n <div class=\"flex flex-col gap-6 mt-2\">\n @for (category of presetCategories(); track category.name) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center gap-2 font-semibold text-sm\">\n <ngs-icon [name]=\"category.icon\" class=\"size-5\"/>\n {{ category.name }}\n </div>\n\n <div class=\"grid grid-cols-3 gap-3\">\n @for (preset of category.presets; track preset.name) {\n <button (click)=\"selectPreset(preset)\"\n class=\"flex flex-col items-center gap-1 p-2 rounded-lg border border-border hover:bg-surface-container transition-colors text-center\">\n <ngs-icon [name]=\"preset.icon\" class=\"size-6 mb-1\"/>\n <span class=\"text-xs font-medium truncate w-full\">{{ preset.name }}</span>\n <span class=\"text-[10px] text-muted-foreground whitespace-nowrap\">{{ preset.width }}\u00D7{{ preset.height }} px</span>\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n\n @if (effectsPortal()) {\n <div class=\"absolute inset-0 z-20 bg-surface-container-lowest\">\n <ng-container [cdkPortalOutlet]=\"effectsPortal()\"/>\n </div>\n }\n </ngs-tab-panel-aside>\n </ngs-tab-panel>\n </ngs-panel-sidebar>\n <ngs-panel-content class=\"h-full\">\n <ngs-panel class=\"h-full\">\n <ngs-panel-header>\n <ngs-toolbar class=\"h-full px-3 border-b border-border\">\n <button ngsIconButton (click)=\"toggleAside()\">\n <ngs-icon name=\"fluent:navigation-24-regular\"/>\n </button>\n <ngs-toolbar-title>{{ title() }}</ngs-toolbar-title>\n <ngs-toolbar-spacer/>\n <div class=\"flex items-center gap-2\">\n <button ngsIconButton (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\"/>\n </button>\n <span class=\"text-sm font-medium w-12 text-center\">{{ zoomPercentage() }}%</span>\n <button ngsIconButton (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\"/>\n </button>\n </div>\n <ngs-toolbar-spacer/>\n <div class=\"flex\">\n <button ngsIconButton (click)=\"undo()\" [disabled]=\"!designerService.canUndo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"redo()\" [disabled]=\"!designerService.canRedo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-right-24-regular\"/>\n </button>\n </div>\n @if (showDownloadButton()) {\n <ngs-divider vertical/>\n <button ngsButton=\"filled\" (click)=\"download()\" class=\"mr-2\">Download</button>\n }\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"bg-surface-container overflow-hidden h-full relative\" (wheel)=\"onWheel($event)\"\n (mousedown)=\"canvasContainer.focus()\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\">\n @if (settingsPortal()) {\n <ng-container [cdkPortalOutlet]=\"settingsPortal()\"/>\n }\n <div #canvasContainer class=\"w-full h-full outline-none\" tabindex=\"0\" (contextmenu)=\"$event.preventDefault()\"></div>\n </ngs-panel-content>\n </ngs-panel>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;width:100%;height:100%}:host ngs-tab-panel{--ngs-tab-panel-aside-width: calc(var(--spacing, .25rem) * 90);--ngs-tab-panel-nav-padding: calc(var(--spacing, .25rem) * 3) 0}:host ngs-list{--ngs-list-padding: 0;--ngs-list-item-radius: 0}:host ngs-tab-group{--ngs-tab-label-padding: 16px 16px}:host .masonry-grid{column-count:2;column-gap:12px}:host .masonry-grid>*{break-inside:avoid;margin-bottom:12px;display:block;width:100%}:host button.is-dragging{background:transparent!important;border-color:transparent!important;box-shadow:none!important;transition:none!important;outline:none!important}:host button.is-dragging:hover{background:transparent!important;border-color:transparent!important;box-shadow:none!important}:host button.is-dragging *{color:currentColor!important}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelSidebar, selector: "ngs-panel-sidebar" }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: Divider, selector: "ngs-divider", inputs: ["vertical", "inset", "fixedHeight"], exportAs: ["ngsDivider"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: ToolbarTitle, selector: "ngs-toolbar-title" }, { kind: "component", type: TabPanel, selector: "ngs-tab-panel", inputs: ["hideContentIfTabNotSelected", "activeItemId", "compact"], outputs: ["itemIdChanged"], exportAs: ["ngsTabPanel"] }, { kind: "component", type: TabPanelAside, selector: "ngs-tab-panel-aside", exportAs: ["ngsTabPanelAside"] }, { kind: "directive", type: TabPanelAsideContentDirective, selector: "[ngsTabPanelAsideContent]", inputs: ["ngsTabPanelAsideContent"], exportAs: ["ngsTabPanelAsideContent"] }, { kind: "component", type: TabPanelContent, selector: "ngs-tab-panel-content", exportAs: ["ngsTabPanelContent"] }, { kind: "component", type: TabPanelItem, selector: "ngs-tab-panel-item", inputs: ["for"], exportAs: ["ngsTabPanelItem"] }, { kind: "directive", type: TabPanelItemIconDirective, selector: "[ngsTabPanelItemIcon]", exportAs: ["ngsTabPanelItemIcon"] }, { kind: "component", type: TabPanelItemText, selector: "ngs-tab-panel-item-text", exportAs: ["ngsTabPanelItemText"] }, { kind: "component", type: TabPanelNav, selector: "ngs-tab-panel-nav", exportAs: ["ngsTabPanelNav"] }, { kind: "component", type: List, selector: "ngs-list", inputs: ["disabled", "disableRipple"], exportAs: ["ngsList"] }, { kind: "component", type: ListItem, selector: "ngs-list-item, a[ngs-list-item], button[ngs-list-item]", inputs: ["disabled", "lines"], exportAs: ["ngsListItem"] }, { kind: "directive", type: ListItemLine, selector: "[ngsListItemLine], [ngsLine]" }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: TabGroup, selector: "ngs-tab-group", inputs: ["selectedIndex", "headerPosition", "preserveContent", "ngs-stretch-tabs", "ngs-align-tabs", "disableRipple", "animationDuration", "animate.enter", "animate.leave"], outputs: ["selectedIndexChange", "selectedTabChange", "focusChange"] }, { kind: "component", type: Tab, selector: "ngs-tab", inputs: ["label", "aria-label", "aria-labelledby", "disabled"], exportAs: ["ngsTab"] }, { kind: "component", type: Accordion, selector: "ngs-accordion", inputs: ["multi", "hideToggle"], exportAs: ["ngsAccordion"] }, { kind: "component", type: ExpansionPanel, selector: "ngs-expansion-panel", inputs: ["disabled", "expanded", "hideToggle"], outputs: ["expandedChange", "opened", "closed"], exportAs: ["ngsExpansionPanel"] }, { kind: "component", type: ExpansionPanelHeader, selector: "ngs-expansion-panel-header", inputs: ["hideToggle"] }, { kind: "component", type: ExpansionPanelTitle, selector: "ngs-expansion-panel-title", exportAs: ["ngsExpansionPanelTitle"] }, { kind: "component", type: FormField, selector: "ngs-form-field", inputs: ["subscriptHiddenIfEmpty"], exportAs: ["ngsFormField"] }, { kind: "component", type: Label, selector: "ngs-label" }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "component", type: Select, selector: "ngs-select", inputs: ["id", "placeholder", "disabled", "required", "multiple", "hideCheckIcon", "ariaLabel", "tabIndex", "aria-describedby", "value"], outputs: ["selectionChange", "opened", "closed", "valueChange"], exportAs: ["ngsSelect"] }, { kind: "component", type: Option, selector: "ngs-option", inputs: ["value", "disabled", "selected"], outputs: ["onSelectionChange"], exportAs: ["ngsOption"] }, { kind: "component", type: ColorSwitcher, selector: "ngs-color-switcher", inputs: ["colors", "selectedColor", "disabled"], outputs: ["colorChange"], exportAs: ["ngsColorSwitcher"] }, { kind: "component", type: ScrollbarArea, selector: "ngs-scrollbar-area", inputs: ["scrollbarWidth", "autoHide", "absolute"], outputs: ["scrolled"], exportAs: ["ngsScrollbarArea"] }, { kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "directive", type: UploadTriggerDirective, selector: "[ngsUploadTrigger]", inputs: ["accept", "multiple"], outputs: ["fileSelected"], exportAs: ["ngsUploadTrigger"] }, { kind: "component", type: ProgressSpinner, selector: "ngs-progress-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["ngsProgressSpinner"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
3954
|
+
}
|
|
3955
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesigner, decorators: [{
|
|
3956
|
+
type: Component,
|
|
3957
|
+
args: [{ selector: 'ngs-image-designer', exportAs: 'ngsImageDesigner', imports: [
|
|
3958
|
+
Panel,
|
|
3959
|
+
PanelSidebar,
|
|
3960
|
+
PanelContent,
|
|
3961
|
+
PanelHeader,
|
|
3962
|
+
Toolbar,
|
|
3963
|
+
Button,
|
|
3964
|
+
ToolbarSpacer,
|
|
3965
|
+
Divider,
|
|
3966
|
+
Icon,
|
|
3967
|
+
ToolbarTitle,
|
|
3968
|
+
TabPanel,
|
|
3969
|
+
TabPanelAside,
|
|
3970
|
+
TabPanelAsideContentDirective,
|
|
3971
|
+
TabPanelContent,
|
|
3972
|
+
TabPanelItem,
|
|
3973
|
+
TabPanelItemIconDirective,
|
|
3974
|
+
TabPanelItemText,
|
|
3975
|
+
TabPanelNav,
|
|
3976
|
+
List,
|
|
3977
|
+
ListItem,
|
|
3978
|
+
ListItemLine,
|
|
3979
|
+
CdkDropList,
|
|
3980
|
+
CdkDrag,
|
|
3981
|
+
CdkDragPlaceholder,
|
|
3982
|
+
TabGroup,
|
|
3983
|
+
Tab,
|
|
3984
|
+
Accordion,
|
|
3985
|
+
ExpansionPanel,
|
|
3986
|
+
ExpansionPanelHeader,
|
|
3987
|
+
ExpansionPanelTitle,
|
|
3988
|
+
FormField,
|
|
3989
|
+
Label,
|
|
3990
|
+
Input,
|
|
3991
|
+
Select,
|
|
3992
|
+
Option,
|
|
3993
|
+
ColorSwitcher,
|
|
3994
|
+
ScrollbarArea,
|
|
3995
|
+
CdkPortalOutlet,
|
|
3996
|
+
UploadTriggerDirective,
|
|
3997
|
+
ProgressSpinner,
|
|
3998
|
+
FormsModule,
|
|
3999
|
+
], providers: [
|
|
4000
|
+
ImageDesignerService,
|
|
4001
|
+
{
|
|
4002
|
+
provide: IMAGE_DESIGNER,
|
|
4003
|
+
useExisting: forwardRef(() => ImageDesigner)
|
|
4004
|
+
}
|
|
4005
|
+
], host: {
|
|
4006
|
+
'class': 'ngs-image-designer',
|
|
4007
|
+
'(keydown)': 'onKeyDown($event)'
|
|
4008
|
+
}, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-sidebar class=\"border-r border-border h-full\">\n <ngs-tab-panel hideContentIfTabNotSelected [activeItemId]=\"activeItemId()\" class=\"h-full\">\n <ngs-tab-panel-content>\n <ngs-tab-panel-nav>\n <ngs-tab-panel-item for=\"text\" (click)=\"activeItemId.set('text')\">\n <ngs-icon name=\"fluent:text-field-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Text</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"photos\" (click)=\"activeItemId.set('photos')\">\n <ngs-icon name=\"fluent:image-copy-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Photos</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"elements\" (click)=\"activeItemId.set('elements')\">\n <ngs-icon name=\"fluent:shape-subtract-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Elements</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"upload\" (click)=\"activeItemId.set('upload')\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Upload</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"background\" (click)=\"activeItemId.set('background')\">\n <ngs-icon name=\"fluent:grid-dots-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Background</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"layers\" (click)=\"activeItemId.set('layers')\">\n <ngs-icon name=\"fluent:layer-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Layers</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"resize\" (click)=\"activeItemId.set('resize')\">\n <ngs-icon name=\"fluent:resize-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Resize</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n </ngs-tab-panel-nav>\n </ngs-tab-panel-content>\n <ngs-tab-panel-aside class=\"border-s border-border\">\n <ng-template ngsTabPanelAsideContent=\"text\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Text</ngs-panel-header>\n <ngs-panel-content class=\"p-4 flex flex-col gap-4\">\n <div class=\"grid grid-cols-1 gap-4 mt-2\">\n <button (click)=\"addHeader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'header')\"\n class=\"w-full text-left p-4 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-3xl font-bold pointer-events-none\">Add a heading</div>\n </button>\n <button (click)=\"addSubheader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'subheader')\"\n class=\"w-full text-left p-3 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-xl font-semibold pointer-events-none\">Add a subheading</div>\n </button>\n <button (click)=\"addBodyText()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'body')\"\n class=\"w-full text-left p-2 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-base pointer-events-none\">Add body text</div>\n </button>\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"photos\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n<!-- <ngs-form-field class=\"w-full\" subscriptHiddenIfEmpty>-->\n<!-- <ngs-icon name=\"fluent:search-24-regular\" ngsIconPrefix/>-->\n<!-- <input type=\"text\"-->\n<!-- ngsInput-->\n<!-- placeholder=\"Search photos\"-->\n<!-- [ngModel]=\"photosFilter()\"-->\n<!-- (ngModelChange)=\"photosFilter.set($event)\"/>-->\n<!-- </ngs-form-field>-->\n Photos\n </ngs-panel-header>\n <ngs-panel-content class=\"relative flex-1 p-0 overflow-hidden\">\n <ngs-scrollbar-area (scrolled)=\"onPhotosScroll($event)\" class=\"h-full\">\n <div class=\"p-4\">\n <div class=\"masonry-grid\">\n @for (photo of photos(); track photo.id) {\n <button (click)=\"addImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2 hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.thumbUrl || photo.url\"\n [alt]=\"photo.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingPhotos()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n @if (photos().length === 0 && !isLoadingPhotos()) {\n <div class=\"text-center p-4 text-sm text-neutral-500\">\n No photos found\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"elements\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Elements</ngs-panel-header>\n <ngs-panel-content class=\"p-4 overflow-y-auto\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (element of elements(); track element.name) {\n <button (click)=\"addShape(element)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, element)\"\n class=\"aspect-square p-2 border border-border rounded-lg hover:bg-surface-container\n transition-colors flex items-center justify-center\">\n <img [src]=\"element.data\" [alt]=\"element.name\"\n class=\"max-w-full max-h-full object-contain pointer-events-none\"/>\n </button>\n }\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"upload\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n <ngs-toolbar>\n <div>Upload</div>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"filled\"\n ngsUploadTrigger\n [accept]=\"'image/*'\"\n (fileSelected)=\"onFileSelected($event)\"\n [loading]=\"isUploading()\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\"/>\n Add Image\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content>\n @if (uploadedImages().length > 0 || assets().length > 0) {\n <ngs-scrollbar-area (scrolled)=\"onAssetsScroll($event)\">\n <div class=\"p-4 flex flex-col gap-4 min-h-full\">\n @if (isUploading() || uploadPreview()) {\n <div class=\"relative aspect-video border border-border rounded-lg overflow-hidden bg-surface-container\">\n @if (uploadPreview()) {\n <img [src]=\"uploadPreview()\" class=\"w-full h-full object-contain\" alt=\"Preview\"/>\n }\n @if (isUploading()) {\n <div class=\"absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[2px]\">\n <div class=\"flex flex-col items-center gap-2\">\n <ngs-progress-spinner diameter=\"32\" color=\"white\"/>\n <span class=\"text-xs font-medium text-white\">Uploading...</span>\n </div>\n </div>\n }\n </div>\n }\n\n @if (uploadedImages().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Your Uploads</div>\n <div class=\"masonry-grid\">\n @for (photo of uploadedImages(); track $index) {\n <button (click)=\"addUploadedImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.url\" class=\"w-full h-auto block pointer-events-none\" alt=\"Uploaded image\"/>\n </button>\n }\n </div>\n </div>\n }\n\n @if (assets().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Assets</div>\n <div class=\"masonry-grid\">\n @for (asset of assets(); track asset.id) {\n <button (click)=\"addImage(asset)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, asset)\"\n [style.aspect-ratio]=\"asset.width + '/' + asset.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"asset.thumbUrl || asset.url\"\n [alt]=\"asset.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingAssets()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n }\n\n @if (uploadedImages().length === 0 && assets().length === 0 && !isLoadingAssets()) {\n <div class=\"h-full flex items-center justify-center\">\n <div class=\"text-sm text-neutral-500\">No images uploaded yet.</div>\n </div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"background\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Background</ngs-panel-header>\n <ngs-panel-content>\n <ngs-tab-group animationDuration=\"0\" class=\"h-full\">\n <ngs-tab label=\"Color\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <ngs-accordion [multi]=\"false\">\n <ngs-expansion-panel [expanded]=\"true\">\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Colors</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"py-2\">\n <ngs-color-switcher [colors]=\"presetColors()\" (colorChange)=\"setColor($event)\"/>\n </div>\n </ngs-expansion-panel>\n <ngs-expansion-panel>\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Gradients</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"grid grid-cols-3 gap-3 py-2\">\n @for (gradient of presetGradients(); track $index) {\n <button (click)=\"setGradient(gradient)\"\n [style.background]=\"gradient.css\"\n class=\"aspect-[16/6.75] w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all\">\n </button>\n }\n </div>\n </ngs-expansion-panel>\n </ngs-accordion>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n <ngs-tab label=\"Patterns\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (pattern of presetPatterns(); track $index) {\n <button (click)=\"addPattern(pattern)\"\n class=\"aspect-square w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all overflow-hidden\">\n <img [src]=\"pattern\" class=\"w-full h-full object-cover\" alt=\"pattern\">\n </button>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"layers\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Layers</ngs-panel-header>\n <ngs-panel-content>\n @if (layersFromService().length > 0) {\n <ngs-list cdkDropList (cdkDropListDropped)=\"drop($event)\">\n @for (layer of layersFromService(); track layer.id) {\n <ngs-list-item cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragStartDelay]=\"100\"\n (click)=\"selectLayer(layer.id!)\"\n [class.is-active]=\"selectedLayerId() === layer.id\"\n class=\"relative group bg-surface\">\n <ngs-icon [name]=\"getLayerIcon(layer.type)\" ngsListItemIcon/>\n <div ngsListItemLine>\n {{ layer.name || layer.type }}\n <!-- {{ layer.text || '' }}-->\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 flex items-center\">\n <button ngsIconButton (click)=\"toggleLayerVisibility(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.visible === false ? 'fluent:eye-off-24-regular' : 'fluent:eye-24-regular'\"/>\n </button>\n <button ngsIconButton (click)=\"toggleLayerLock(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.locked ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"\n class=\"size-5\"/>\n </button>\n <button ngsIconButton (click)=\"deleteLayer(layer.id!, $event)\" [disabled]=\"layer.locked\">\n <ngs-icon name=\"fluent:delete-24-regular\" class=\"size-5\"/>\n </button>\n </div>\n\n <div *cdkDragPlaceholder\n class=\"min-h-[var(--ngs-list-item-min-height)] bg-surface-container\"></div>\n </ngs-list-item>\n }\n </ngs-list>\n } @else {\n <div class=\"text-sm p-4 h-full flex items-center justify-center\">No layers available</div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"resize\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Resize</ngs-panel-header>\n <ngs-panel-content>\n <ngs-scrollbar-area>\n <div class=\"p-4 flex flex-col gap-4\">\n <div class=\"flex flex-col gap-4\">\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Width (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeWidth\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Height (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeHeight\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Units</ngs-label>\n <ngs-select [(ngModel)]=\"resizeUnit\">\n <ngs-option value=\"px\">px</ngs-option>\n </ngs-select>\n </ngs-form-field>\n\n <button ngsButton=\"filled\" fullWidth (click)=\"applyResize()\">Resize</button>\n </div>\n\n <div class=\"flex flex-col gap-6 mt-2\">\n @for (category of presetCategories(); track category.name) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center gap-2 font-semibold text-sm\">\n <ngs-icon [name]=\"category.icon\" class=\"size-5\"/>\n {{ category.name }}\n </div>\n\n <div class=\"grid grid-cols-3 gap-3\">\n @for (preset of category.presets; track preset.name) {\n <button (click)=\"selectPreset(preset)\"\n class=\"flex flex-col items-center gap-1 p-2 rounded-lg border border-border hover:bg-surface-container transition-colors text-center\">\n <ngs-icon [name]=\"preset.icon\" class=\"size-6 mb-1\"/>\n <span class=\"text-xs font-medium truncate w-full\">{{ preset.name }}</span>\n <span class=\"text-[10px] text-muted-foreground whitespace-nowrap\">{{ preset.width }}\u00D7{{ preset.height }} px</span>\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n\n @if (effectsPortal()) {\n <div class=\"absolute inset-0 z-20 bg-surface-container-lowest\">\n <ng-container [cdkPortalOutlet]=\"effectsPortal()\"/>\n </div>\n }\n </ngs-tab-panel-aside>\n </ngs-tab-panel>\n </ngs-panel-sidebar>\n <ngs-panel-content class=\"h-full\">\n <ngs-panel class=\"h-full\">\n <ngs-panel-header>\n <ngs-toolbar class=\"h-full px-3 border-b border-border\">\n <button ngsIconButton (click)=\"toggleAside()\">\n <ngs-icon name=\"fluent:navigation-24-regular\"/>\n </button>\n <ngs-toolbar-title>{{ title() }}</ngs-toolbar-title>\n <ngs-toolbar-spacer/>\n <div class=\"flex items-center gap-2\">\n <button ngsIconButton (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\"/>\n </button>\n <span class=\"text-sm font-medium w-12 text-center\">{{ zoomPercentage() }}%</span>\n <button ngsIconButton (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\"/>\n </button>\n </div>\n <ngs-toolbar-spacer/>\n <div class=\"flex\">\n <button ngsIconButton (click)=\"undo()\" [disabled]=\"!designerService.canUndo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"redo()\" [disabled]=\"!designerService.canRedo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-right-24-regular\"/>\n </button>\n </div>\n @if (showDownloadButton()) {\n <ngs-divider vertical/>\n <button ngsButton=\"filled\" (click)=\"download()\" class=\"mr-2\">Download</button>\n }\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"bg-surface-container overflow-hidden h-full relative\" (wheel)=\"onWheel($event)\"\n (mousedown)=\"canvasContainer.focus()\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\">\n @if (settingsPortal()) {\n <ng-container [cdkPortalOutlet]=\"settingsPortal()\"/>\n }\n <div #canvasContainer class=\"w-full h-full outline-none\" tabindex=\"0\" (contextmenu)=\"$event.preventDefault()\"></div>\n </ngs-panel-content>\n </ngs-panel>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;width:100%;height:100%}:host ngs-tab-panel{--ngs-tab-panel-aside-width: calc(var(--spacing, .25rem) * 90);--ngs-tab-panel-nav-padding: calc(var(--spacing, .25rem) * 3) 0}:host ngs-list{--ngs-list-padding: 0;--ngs-list-item-radius: 0}:host ngs-tab-group{--ngs-tab-label-padding: 16px 16px}:host .masonry-grid{column-count:2;column-gap:12px}:host .masonry-grid>*{break-inside:avoid;margin-bottom:12px;display:block;width:100%}:host button.is-dragging{background:transparent!important;border-color:transparent!important;box-shadow:none!important;transition:none!important;outline:none!important}:host button.is-dragging:hover{background:transparent!important;border-color:transparent!important;box-shadow:none!important}:host button.is-dragging *{color:currentColor!important}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
4009
|
+
}], ctorParameters: () => [], propDecorators: { canvasContainer: [{ type: i0.ViewChild, args: ['canvasContainer', { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], imageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageSize", required: false }] }], defaultFont: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultFont", required: false }] }], scale: [{ type: i0.Input, args: [{ isSignal: true, alias: "scale", required: false }] }], minScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "minScale", required: false }] }], maxScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxScale", required: false }] }], showGuidelines: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGuidelines", required: false }] }], snapToShapes: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToShapes", required: false }] }], snapToStageCenter: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToStageCenter", required: false }] }], snapToStageBorders: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToStageBorders", required: false }] }], guidelineColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "guidelineColor", required: false }] }], snapRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapRange", required: false }] }], showDownloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDownloadButton", required: false }] }], uploadFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFn", required: false }] }], snapshot: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapshot", required: false }] }], snapshotChanged: [{ type: i0.Output, args: ["snapshotChanged"] }], photosDataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "photosDataSource", required: false }] }], assetsDataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "assetsDataSource", required: false }] }], historyLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "historyLimit", required: false }] }] } });
|
|
4010
|
+
|
|
4011
|
+
/**
|
|
4012
|
+
* Generated bundle index. Do not edit.
|
|
4013
|
+
*/
|
|
4014
|
+
|
|
4015
|
+
export { IMAGE_DESIGNER, ImageDesigner, ImageDesignerService, SVG_ELEMENTS, SVG_PATTERNS, Settings, createDefaultPhotosDataSource };
|
|
4016
|
+
//# sourceMappingURL=ngstarter-ui-components-image-designer.mjs.map
|