diff --git a/.gitignore b/.gitignore index c8a06d5..86c4cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vs/* node_modules/* *.stg +package-lock.json +package.json diff --git a/README.md b/README.md index a3c38dd..b8d91d7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Pseudo-vector image creator. ## [Launch Application](https://malulleybovo.github.io/SymbolArtEditorOnline/) - #### Version 2.0.1 + #### Version 2.1.0 ### Quickly get down to business and make art with fluidity and agility.
Symbol Art Editor was designed based on three pillars: simplicity, ease of use, and agility. diff --git a/index.html b/index.html index f780293..bb0c7fb 100644 --- a/index.html +++ b/index.html @@ -103,8 +103,8 @@ - - + + diff --git a/res/templates/container.html b/res/templates/container.html deleted file mode 100644 index 41d60fb..0000000 --- a/res/templates/container.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/res/templates/containerpicker.html b/res/templates/containerpicker.html deleted file mode 100644 index 91e9b40..0000000 --- a/res/templates/containerpicker.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- 0/0 Symbols -
- 0/0 Containers -
-
-
\ No newline at end of file diff --git a/res/templates/copyrightview.html b/res/templates/copyrightview.html index ce49e6c..aa285e6 100644 --- a/res/templates/copyrightview.html +++ b/res/templates/copyrightview.html @@ -1,3 +1,3 @@ -
+
by malulleybovo
\ No newline at end of file diff --git a/res/templates/layer.html b/res/templates/layer.html new file mode 100644 index 0000000..b3dfa9c --- /dev/null +++ b/res/templates/layer.html @@ -0,0 +1,20 @@ +
+
+ +
+
+ + + + + + +
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/res/templates/layerpicker.html b/res/templates/layerpicker.html new file mode 100644 index 0000000..62f03ab --- /dev/null +++ b/res/templates/layerpicker.html @@ -0,0 +1,18 @@ +
+
+ 0/0 Symbols +
+ 0/0 Containers +
+
+
+
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/res/templates/menu.html b/res/templates/menu.html index 8d53adb..1bf3e14 100644 --- a/res/templates/menu.html +++ b/res/templates/menu.html @@ -3,11 +3,14 @@
-
- +
+ +
+
+
- +
SYMBOL ART
\ No newline at end of file diff --git a/res/templates/optionsview.html b/res/templates/optionsview.html index 13d4e50..5cde256 100644 --- a/res/templates/optionsview.html +++ b/res/templates/optionsview.html @@ -1,6 +1,6 @@
-
v2.0.1
© 2021 Copyright malulleybovo
+
v2.1.0
© 2021 Copyright malulleybovo
diff --git a/src/UIApplication.js b/src/UIApplication.js index 53779b0..d24d703 100644 --- a/src/UIApplication.js +++ b/src/UIApplication.js @@ -67,6 +67,9 @@ class UIApplication { } else if (originalColorInContainer) { this._renderer.presenter.setColorForSelectedContainer({ originalColorInContainer: originalColorInContainer, hexValue: hexValue, opacity: opacity, updatesHistory: lastInteraction }); } + if (lastInteraction) { + this._layerPicker.updateWith({ container: this._symbolArt.root }); + } }, requestedEyeDropperColors: _ => { this._symbolArt.helperImage.imageColors().then(colors => { @@ -88,15 +91,55 @@ class UIApplication { }, onAssetChange: (selectedAsset) => { this._renderer.presenter.setAssetForSelectedSymbol({ asset: selectedAsset }); + this._layerPicker.updateWith({ container: this._symbolArt.root }); } }); - _containerPicker = new UIContainerPicker({ - onContainerSelected: (containerUuid) => { - let container3D = Layer3D.layersInUse[containerUuid]; - if (container3D instanceof Container3D) { - this._renderer.setSelection({ layer3D: container3D }); + _layerPicker = new UILayerPicker({ + onLayerSelected: (layerUuid) => { + let layer3D = Layer3D.layersInUse[layerUuid]; + if (layer3D instanceof Layer3D) { + this._renderer.setSelection({ layer3D: layer3D }); this._renderer.panToSelection(); + SymbolControls3D.shared.attach({ toSymbol3D: this._renderer.selectionGroup }); + ContainerControls3D.shared.attach({ toContainer3D: this._renderer.selectionGroup }); + HelperImageControls3D.shared.attach({ toHelperImage: null }); } + }, + onRenameTapped: layerUuid => { + let layerToRename = null; + this._symbolArt.root.depthFirstIterator(layer => { + if (layer.uuid === layerUuid) { + layerToRename = layer; + return true; + } + }); + if (!(layerToRename instanceof Layer)) return; + new UIModalTextField({ + title: 'Rename layer:', initialText: layerToRename.name, + onInput: text => { }, + onResult: text => { + if (layerToRename.name == text) return; + layerToRename.name = text; + this._renderer.updateWith({ symbolArt: this._symbolArt }); + HistoryState.shared.pushHistory({ data: this._symbolArt.clone() }); + } + }); + }, + onDeleteTapped: layerUuid => { + let layerToDelete = null; + this._symbolArt.root.depthFirstIterator(layer => { + if (layer.uuid === layerUuid) { + layerToDelete = layer; + return true; + } + }); + if (!(layerToDelete instanceof Layer) && layerToDelete.parent) return; + layerToDelete.parent.remove({ sublayer: layerToDelete }); + this._renderer.updateWith({ symbolArt: this._symbolArt }); + HistoryState.shared.pushHistory({ data: this._symbolArt.clone() }); + SymbolControls3D.shared.attach({ toSymbol3D: this._renderer.selectionGroup }); + ContainerControls3D.shared.attach({ toContainer3D: this._renderer.selectionGroup }); + HelperImageControls3D.shared.attach({ toHelperImage: null }); } }); _helperImageSettings = new UIHelperImageSettings({ @@ -139,7 +182,10 @@ class UIApplication { _renderer = (() => { let renderer = new Renderer(); renderer.onSymbolArtChanged = () => { - this._containerPicker.updateWith({ containers: this._symbolArt.root.containers }); + this._layerPicker.updateWith({ container: this._symbolArt.root }); + setTimeout(_ => { + this._layerPicker.select({ layerWithUuid: this._renderer.selectionUuid }); + }, 10); }; renderer.onSelectionChanged = (selectionUuid) => { let selectionLayer = this._symbolArt.findLayer({ withUuidString: selectionUuid }); @@ -149,6 +195,7 @@ class UIApplication { this._actionBar.setNorthEastButton({ enabled: selectedLayer, forViewMode: ViewMode.symbolEditorMode }); this._actionBar.setEastButton({ enabled: selectedLayer, forViewMode: ViewMode.symbolEditorMode }); this._assetPicker.assetSelectionEnabled = selectedSymbol; + this._layerPicker.select({ layerWithUuid: selectionUuid }); if (!selectedLayer) return; if (selectedSymbol) { this._assetPicker.update({ @@ -246,7 +293,7 @@ class UIApplication { if (!this.loaded) return; let $body = $('body'); this._copyrightView.append({ to: $body }); - this._containerPicker.append({ to: $body }); + this._layerPicker.append({ to: $body }); this._menu.append({ to: $body }); this._actionBar.append({ to: $body }); this._colorPicker.append({ to: $body }); diff --git a/src/components/InputDevice.js b/src/components/InputDevice.js index f9a53ec..eadd6f9 100644 --- a/src/components/InputDevice.js +++ b/src/components/InputDevice.js @@ -39,12 +39,22 @@ class InputDevice { _tapDuration = 500; _tapTimeout = null; - _longTapDuration = 750; + _longTapDuration = 600; _longTapTimeout = null; _longStart = false; get longStart() { return this._longStart } + _longTouchesWithoutDelay = false; + get longTouchesWithoutDelay() { + return this._longTouchesWithoutDelay; + } + set longTouchesWithoutDelay(value) { + if (typeof value === 'boolean') { + this._longTouchesWithoutDelay = value; + } + } + _previousPinchLength = -1; _minimumNumberOfPointsToTriggerMotion = 10; @@ -89,7 +99,7 @@ class InputDevice { this._longTapTimeout = null; this._longStart = true; this.longTouchBegan(longEvent); - }, this._longTapDuration); + }, this._longTouchesWithoutDelay ? 25 : this._longTapDuration); } this.touchBegan(event, this._activeTouchEvents.length); } @@ -147,6 +157,19 @@ class InputDevice { _interactionEnded(event) { if (!UIApplication.shared.loaded) return; + let longStart = this._longStart; + if (this._longTapTimeout !== null) { + if (this._longTouchesWithoutDelay) { + setTimeout(_ => { + this._interactionEnded(event); + }); + return; + } else { + clearTimeout(this._longTapTimeout); + this._longTapTimeout = null; + this._longStart = false; + } + } if (event && event.type === 'blur') { this._activeTouchEvents = []; } @@ -165,16 +188,11 @@ class InputDevice { } this._activeEventState = 0; this._originalTouchEvent = null; - if (this._longStart) { + if (longStart) { this.longTouchEnded(event); } else { this.touchEnded(event); } - if (this._longTapTimeout !== null) { - clearTimeout(this._longTapTimeout); - this._longTapTimeout = null; - this._longStart = false; - } } _scrolled(event) { diff --git a/src/components/ui/UIAssetPicker.js b/src/components/ui/UIAssetPicker.js index 88ce554..71cbcae 100644 --- a/src/components/ui/UIAssetPicker.js +++ b/src/components/ui/UIAssetPicker.js @@ -206,7 +206,6 @@ class UIAssetPicker extends UIView { this._assetPreview.attr('src', ''); return; } - this._assetSelected({ uiAsset: this._catalog[asset.filePath].uiAsset }); } updateState() { diff --git a/src/components/ui/UIColorPicker.js b/src/components/ui/UIColorPicker.js index 4db47eb..b4531c0 100644 --- a/src/components/ui/UIColorPicker.js +++ b/src/components/ui/UIColorPicker.js @@ -407,6 +407,7 @@ class UIColorPicker extends UIView { } }); this._colorPaletteList.empty(); + this._colorPalette.forEach(a => a.remove()); this._colorPalette = []; Object.values(occurrences).forEach(a => { let subview = new UIColorPreview({ diff --git a/src/components/ui/UIContainer.js b/src/components/ui/UIContainer.js deleted file mode 100644 index 83d8d3f..0000000 --- a/src/components/ui/UIContainer.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Symbol Art Editor - * - * @author malulleybovo (since 2021) - * @license GNU General Public License v3.0 - * - * @licstart The following is the entire license notice for the - * JavaScript code in this page. - * - * Copyright (C) 2021 Arthur Malulley B. de O. - * - * - * The JavaScript code in this page is free software: you can - * redistribute it and/or modify it under the terms of the GNU - * General Public License (GNU GPL) as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) - * any later version. The code is distributed WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute non-source (e.g., minimized or compacted) forms of - * that code without the copy of the GNU GPL normally required by - * section 4, provided you include this license notice and a URL - * through which recipients can access the Corresponding Source. - * - * @licend The above is the entire license notice - * for the JavaScript code in this page. - * - */ - -class UIContainer extends UIView { - - get viewPath() { return 'res/templates/container.html' } - - _onTap = null; - get onTap() { return this._onTap } - set onTap(value) { - if (typeof value !== 'function' && value !== null) return; - this._onTap = value; - } - - get name() { - if (!this.loaded) return ''; - return this.view.text(); - } - set name(value) { - if (typeof value === 'string') { - this.didLoad(_ => { - this.view.text(value); - }); - } - } - - _identifier = null; - get identifier() { - return this._identifier; - } - set identifier(value) { - if (typeof value === 'string') { - this._identifier = value; - } - } - - get loaded() { - return this.view instanceof jQuery - && this.view[0] instanceof HTMLElement; - } - - constructor({ filePath = null } = {}) { - super(); - let path = filePath; - this.didLoad(_ => { - if (mobileClient) this.view.css('margin-right', '8px'); - this.view.attr('src', path); - this.gestureRecognizer = new UITapGestureRecognizer({ - targetHtmlElement: this.view[0], onTap: () => { - if (this._onTap) { - this._onTap(this); - } - } - }); - }); - } - -} diff --git a/src/components/ui/UIContainerPicker.js b/src/components/ui/UIContainerPicker.js deleted file mode 100644 index 8bf7dc6..0000000 --- a/src/components/ui/UIContainerPicker.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Symbol Art Editor - * - * @author malulleybovo (since 2021) - * @license GNU General Public License v3.0 - * - * @licstart The following is the entire license notice for the - * JavaScript code in this page. - * - * Copyright (C) 2021 Arthur Malulley B. de O. - * - * - * The JavaScript code in this page is free software: you can - * redistribute it and/or modify it under the terms of the GNU - * General Public License (GNU GPL) as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) - * any later version. The code is distributed WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute non-source (e.g., minimized or compacted) forms of - * that code without the copy of the GNU GPL normally required by - * section 4, provided you include this license notice and a URL - * through which recipients can access the Corresponding Source. - * - * @licend The above is the entire license notice - * for the JavaScript code in this page. - * - */ - -class UIContainerPicker extends UIView { - - get viewPath() { return 'res/templates/containerpicker.html' } - - _listView = (() => { - this.didLoad(_ => { - this._listView = this.view.find('#listview'); - this.updateState(); - }); - })(); - - _symbolCount = (() => { - this.didLoad(_ => { - this._symbolCount = this.view.find('#symbolcount'); - this.updateState(); - }); - })(); - - _symbolTotal = (() => { - this.didLoad(_ => { - this._symbolTotal = this.view.find('#symboltotal'); - this.updateState(); - }); - })(); - - _containerCount = (() => { - this.didLoad(_ => { - this._containerCount = this.view.find('#containercount'); - this.updateState(); - }); - })(); - - _containerTotal = (() => { - this.didLoad(_ => { - this._containerTotal = this.view.find('#containertotal'); - this.updateState(); - }); - })(); - - _onContainerSelected = null; - set onContainerSelected(value) { - if (typeof value !== 'function' && value !== null) return; - this._onContainerSelected = value; - } - - get loaded() { - return this.view instanceof jQuery - && this.view[0] instanceof HTMLElement - && this._listView instanceof jQuery - && this._symbolCount instanceof jQuery - && this._symbolTotal instanceof jQuery - && this._containerCount instanceof jQuery - && this._containerTotal instanceof jQuery; - } - - set isHidden(value) { - if (typeof value !== 'boolean') return; - this.didLoad(_ => { - this.view.css('right', value ? '-200px' : '0px'); - }); - } - - constructor({ onContainerSelected = null } = { }) { - super(); - this.onContainerSelected = onContainerSelected; - this.didLoad(_ => { - this.view.gestureRecognizer = new UIGestureRecognizer({ - targetHtmlElement: this.view[0], - preventsDefault: false, - onPointerDown: (event) => { - // to prevent propagation - }, onPointerMove: (event) => { - // to prevent propagation - }, onPointerUp: (event) => { - // to prevent propagation - }, onScroll: (event) => { - // to prevent propagation - }, onKeyPress: (event) => { - return false; - } - }); - }); - ApplicationState.shared.add({ - onChangeViewModeListener: () => { - this.updateState(); - } - }); - this.updateState(); - } - - _uiContainers = []; - - updateWith({ containers }) { - if (!(Array.isArray(containers))) return; - if (!this.loaded) { - this.didLoad(_ => { - this.updateWith({ containers: containers }); - }); - return; - } - let uiContainers = []; - for (var index in containers) { - let container = containers[index]; - if (!(container instanceof Container)) continue; - let matches = this._uiContainers.filter(a => a.identifier === container.uuid); - let uiContainer = null; - if (matches[0]) { - uiContainer = matches[0]; - } else { - uiContainer = new UIContainer(); - } - uiContainer.name = container.name.length === 0 ? 'container' : container.name; - uiContainer.identifier = container.uuid; - uiContainer.onTap = (uiContainer) => { - if (this._onContainerSelected) { - this._onContainerSelected(uiContainer.identifier); - } - }; - uiContainers.push(uiContainer); - } - let requiresUpdate = false; - for (var index in uiContainers) { - if (this._uiContainers[index] !== uiContainers[index]) { - requiresUpdate = true; - break; - } - } - if (!requiresUpdate) { - for (var index in this._uiContainers) { - if (this._uiContainers[index] !== uiContainers[index]) { - requiresUpdate = true; - break; - } - } - } - if (requiresUpdate) { - this._listView.empty(); - this._uiContainers = uiContainers; - for (var index in this._uiContainers) { - this._uiContainers[index].append({ to: this._listView }); - } - } - this.updateLimits(); - } - - updateLimits() { - if (!this.loaded) { - this.didLoad(_ => { - this.updateLimits(); - }); - return; - } - this._symbolCount.text(`${UIApplication.shared.symbolArt.root.numberOfSymbols}`); - this._symbolTotal.text(`${SymbolArt.maximumNumberOfSymbols}`); - this._containerCount.text(`${UIApplication.shared.symbolArt.root.containers.length}`); - this._containerTotal.text(`${SymbolArt.maximumNumberOfContainers}`); - } - - updateState() { - switch (ApplicationState.shared.viewMode) { - case ViewMode.layerEditorMode: - this.isHidden = false; - break; - case ViewMode.symbolEditorMode: - case ViewMode.helperImageMode: - default: - this.isHidden = true; - return; - } - } - -} diff --git a/src/components/ui/UILayer.js b/src/components/ui/UILayer.js new file mode 100644 index 0000000..f7a7926 --- /dev/null +++ b/src/components/ui/UILayer.js @@ -0,0 +1,245 @@ +/** + * Symbol Art Editor + * + * @author malulleybovo (since 2021) + * @license GNU General Public License v3.0 + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2021 Arthur Malulley B. de O. + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + * + */ + +class UILayer extends UIView { + + get viewPath() { return 'res/templates/layer.html' } + + _onTap = null; + get onTap() { return this._onTap } + set onTap(value) { + if (typeof value !== 'function' && value !== null) return; + this._onTap = value; + } + + get name() { + if (!this.loaded) return ''; + return this._textView.text(); + } + set name(value) { + if (typeof value === 'string') { + this.didLoad(_ => { + this._textView.text(value); + }); + } + } + + _identifier = null; + get identifier() { + return this._identifier; + } + set identifier(value) { + if (typeof value === 'string') { + this._identifier = value; + } + } + + set previewAsset(value) { + if (value instanceof Asset) { + this.didLoad(_ => { + this._symbolPreview.find('image').attr('href', value.filePath); + }); + } + } + + set previewAssetColor(value) { + if (value instanceof Color) { + this.didLoad(_ => { + let hsv = value.hsv; + this._symbolPreview.find('feColorMatrix').attr( + 'values', + (value.r / Color.upperBound) + ' 0 0 0 0 0 ' + + (value.g / Color.upperBound) + ' 0 0 0 0 0 ' + + (value.b / Color.upperBound) + ' 0 0 0 0 0 1 0'); + }); + } + } + + _level = 0; + get level() { + return this._level; + } + set level(value) { + if (Number.isSafeInteger(value)) { + this._level = value; + this.updateState(); + } + } + + _collapsible = true; + get collapsible() { + return this._collapsible; + } + set collapsible(value) { + if (typeof value === 'boolean') { + this._collapsible = value; + this.updateState(); + } + } + + _collapsed = false; + get collapsed() { + return this._collapsed; + } + + _onCollapseChanged = null; + set onCollapseChanged(value) { + if (typeof value === 'function') { + this._onCollapseChanged = value; + } + } + + _selected = false; + get selected() { + return this._selected; + } + set selected(value) { + if (typeof value === 'boolean') { + this._selected = value; + this.updateState(); + } + } + + _onRenameTapped = null; + set onRenameTapped(value) { + if (typeof value !== 'function' && value !== null) return; + this._onRenameTapped = value; + } + + _onDeleteTapped = null; + set onDeleteTapped(value) { + if (typeof value !== 'function' && value !== null) return; + this._onDeleteTapped = value; + } + + get loaded() { + return this.view instanceof jQuery + && this.view[0] instanceof HTMLElement + && this._textView instanceof jQuery + && this._collapseButton instanceof jQuery + && this._symbolPreview instanceof jQuery + && this._renameButton instanceof jQuery + && this._deleteButton instanceof jQuery; + } + + _collapseButton = (() => { + this.didLoad(_ => { + this._collapseButton = this.view.find('#collapsebutton'); + this._collapseButton.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this._collapseButton[0], onTap: () => { + this._collapsed = !this._collapsed; + this._collapseButton.css('background', this._collapsed ? '#101010' : 'white'); + let icon = this._collapseButton.find('i'); + if (this._collapsed) { + icon.removeClass('fa-angle-down'); + icon.addClass('fa-angle-up'); + } else { + icon.removeClass('fa-angle-up'); + icon.addClass('fa-angle-down'); + } + if (this._onCollapseChanged) { + this._onCollapseChanged(this); + } + } + }); + }); + })(); + + _symbolPreview = (() => { + this.didLoad(_ => { + this._symbolPreview = this.view.find('#symbolpreview'); + }); + })(); + + _textView = (() => { + this.didLoad(_ => { + this._textView = this.view.find('#textview'); + }); + })(); + + _renameButton = (() => { + this.didLoad(_ => { + this._renameButton = this.view.find('#renamebutton'); + this._renameButton.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this._renameButton[0], onTap: () => { + if (this._onRenameTapped) { + this._onRenameTapped(this); + } + } + }); + }); + })(); + + _deleteButton = (() => { + this.didLoad(_ => { + this._deleteButton = this.view.find('#deletebutton'); + this._deleteButton.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this._deleteButton[0], onTap: () => { + if (this._onDeleteTapped) { + this._onDeleteTapped(this); + } + } + }); + }); + })(); + + constructor({ filePath = null } = {}) { + super(); + let path = filePath; + this.didLoad(_ => { + if (mobileClient) this.view.css('margin-right', '8px'); + this.view.attr('src', path); + this.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this.view[0], onTap: () => { + if (this._onTap) { + this._onTap(this); + } + } + }); + this.view.find('#placeholderid').attr('id', this.uuidString + '-assetpreview'); + this.view.find('image').attr('filter', 'url(#' + this.uuidString + '-assetpreview)'); + }); + } + + updateState() { + if (!this.loaded) { + this.didLoad(_ => { + this.updateState(); + }); + return; + } + this._textView.css('color', this._selected ? '#ff9e2c' : ''); + this._collapseButton.css('visibility', this._collapsible ? '' : 'hidden'); + this._symbolPreview.css('visibility', this._collapsible ? 'hidden' : ''); + this.view.css('margin-left', (this._level * 12) + 'px'); + } + +} diff --git a/src/components/ui/UILayerPicker.js b/src/components/ui/UILayerPicker.js new file mode 100644 index 0000000..1beedf8 --- /dev/null +++ b/src/components/ui/UILayerPicker.js @@ -0,0 +1,365 @@ +/** + * Symbol Art Editor + * + * @author malulleybovo (since 2021) + * @license GNU General Public License v3.0 + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2021 Arthur Malulley B. de O. + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + * + */ + +class UILayerPicker extends UIView { + + get viewPath() { return 'res/templates/layerpicker.html' } + + _listView = (() => { + this.didLoad(_ => { + this._listView = this.view.find('#listview'); + this.updateState(); + }); + })(); + + _symbolCount = (() => { + this.didLoad(_ => { + this._symbolCount = this.view.find('#symbolcount'); + this.updateState(); + }); + })(); + + _symbolTotal = (() => { + this.didLoad(_ => { + this._symbolTotal = this.view.find('#symboltotal'); + this.updateState(); + }); + })(); + + _containerCount = (() => { + this.didLoad(_ => { + this._containerCount = this.view.find('#containercount'); + this.updateState(); + }); + })(); + + _containerTotal = (() => { + this.didLoad(_ => { + this._containerTotal = this.view.find('#containertotal'); + this.updateState(); + }); + })(); + + _layerSearchContainer = (() => { + this.didLoad(_ => { + this._layerSearchContainer = this.view.find('#layersearchcontainer'); + }); + })(); + + _layerSearchTextField = (() => { + this.didLoad(_ => { + this._layerSearchTextField = this.view.find('#layersearchtextfield'); + if (mobileClient) { + this._layerSearchTextField.helper = new UITextFieldHelper({ + view: this._layerSearchTextField, + title: 'Search layer name:', + onInput: (value) => { + this._updateSearchBar(); + return value; + } + }); + } + this.updateState(); + this._layerSearchTextField.on('input', () => { + this._updateSearchBar(); + }); + }); + })(); + + _searchButton = (() => { + this.didLoad(_ => { + this._searchButton = this.view.find('#searchbutton'); + this._searchButton.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this._searchButton[0], onTap: () => { + if (this._layerSearchTextField.val().trim().length > 0) { + this._layerSearchTextField.val(''); + this._updateSearchBar(); + } + } + }); + }); + })(); + + _onLayerSelected = null; + set onLayerSelected(value) { + if (typeof value !== 'function' && value !== null) return; + this._onLayerSelected = value; + } + + _onRenameTapped = null; + set onRenameTapped(value) { + if (typeof value !== 'function' && value !== null) return; + this._onRenameTapped = value; + } + + _onDeleteTapped = null; + set onDeleteTapped(value) { + if (typeof value !== 'function' && value !== null) return; + this._onDeleteTapped = value; + } + + _lastSelectedUuid = null; + + get loaded() { + return this.view instanceof jQuery + && this.view[0] instanceof HTMLElement + && this._listView instanceof jQuery + && this._symbolCount instanceof jQuery + && this._symbolTotal instanceof jQuery + && this._containerCount instanceof jQuery + && this._containerTotal instanceof jQuery + && this._layerSearchContainer instanceof jQuery + && this._layerSearchTextField instanceof jQuery; + } + + set isHidden(value) { + if (typeof value !== 'boolean') return; + this.didLoad(_ => { + this.view.css('right', value ? ('-' + this.view.css('width')) : '0px'); + }); + } + + constructor({ onLayerSelected = null, onRenameTapped = null, onDeleteTapped = null } = { }) { + super(); + this.onLayerSelected = onLayerSelected; + this.onRenameTapped = onRenameTapped; + this.onDeleteTapped = onDeleteTapped; + this.didLoad(_ => { + this.view.gestureRecognizer = new UIGestureRecognizer({ + targetHtmlElement: this.view[0], + preventsDefault: false, + onPointerDown: (event) => { + // to prevent propagation + }, onPointerMove: (event) => { + // to prevent propagation + }, onPointerUp: (event) => { + // to prevent propagation + }, onScroll: (event) => { + // to prevent propagation + }, onKeyPress: (event) => { + return false; + } + }); + }); + ApplicationState.shared.add({ + onChangeViewModeListener: () => { + this.updateState(); + } + }); + this.updateState(); + } + + _uiLayers = []; + + updateWith({ container }) { + if (!(container instanceof Container)) return; + if (!this.loaded) { + this.didLoad(_ => { + this.updateWith({ container: container }); + }); + return; + } + let layers = []; + container.reverseDepthFirstIterator(layer => { + layers.push(layer); + }); + let uiLayers = []; + for (var index in layers) { + let layer = layers[index]; + let matches = this._uiLayers.filter(a => a.identifier === layer.uuid); + let uiLayer = null; + if (matches[0]) { + uiLayer = matches[0]; + } else { + uiLayer = new UILayer(); + } + uiLayer.name = layer.name.length === 0 ? (layer instanceof Container ? 'container' : 'symbol') : layer.name; + uiLayer.identifier = layer.uuid; + uiLayer.onTap = (uiLayer) => { + if (this._onLayerSelected) { + this._onLayerSelected(uiLayer.identifier); + } + }; + uiLayer.level = layer.distanceFromRoot; + uiLayer.collapsible = layer instanceof Container; + if (layer instanceof Symbol) { + uiLayer.previewAsset = layer.asset; + uiLayer.previewAssetColor = layer.color; + } + uiLayer.onCollapseChanged = instance => { + this._updateCollapsibles(); + }; + uiLayer.onRenameTapped = instance => { + if (this._onRenameTapped) { + this._onRenameTapped(instance.identifier); + } + }; + uiLayer.onDeleteTapped = instance => { + if (this._onDeleteTapped) { + this._onDeleteTapped(instance.identifier); + } + }; + uiLayers.push(uiLayer); + } + let requiresUpdate = false; + for (var index in uiLayers) { + if (this._uiLayers[index] !== uiLayers[index]) { + requiresUpdate = true; + break; + } + } + if (!requiresUpdate) { + for (var index in this._uiLayers) { + if (this._uiLayers[index] !== uiLayers[index]) { + requiresUpdate = true; + break; + } + } + } + this._layerSearchTextField.val(''); + if (requiresUpdate) { + this._uiLayers = uiLayers; + this._listView.empty(); + for (var index in this._uiLayers) { + this._uiLayers[index].append({ to: this._listView }); + } + this._layerSearchContainer.css('display', ''); + if (uiLayers.length < 2) { + this._layerSearchContainer.css('display', 'none'); + } + setTimeout(_ => { + this._updateCollapsibles(); + }, 10); + } else { + this._updateCollapsibles(); + } + this.updateLimits(); + } + + _updateCollapsibles() { + let collapsedAtLevel = -1; + for (var index in this._uiLayers) { + this._uiLayers[index].view.css('display', ''); + let searchKeys = this._layerSearchTextField.val().trim(); + if (searchKeys.length > 0) { + if (!this._uiLayers[index].name.toUpperCase().includes(searchKeys.toUpperCase())) { + this._uiLayers[index].view.css('display', 'none'); + } + } else { + if (collapsedAtLevel >= 0) { + if (this._uiLayers[index].level > collapsedAtLevel) { + this._uiLayers[index].view.css('display', 'none'); + } else { + collapsedAtLevel = -1; + } + } + if (this._uiLayers[index].collapsed) { + collapsedAtLevel = this._uiLayers[index].level; + } + } + } + } + + _updateSearchBar() { + if (!this.loaded) { + this.didLoad(_ => { + this._updateSearchBar(); + }); + return; + } + let icon = this._searchButton.find('i'); + if (this._layerSearchTextField.val().trim().length > 0) { + icon.removeClass('fa-search'); + icon.addClass('fa-times'); + } else { + icon.removeClass('fa-times'); + icon.addClass('fa-search'); + } + this._updateCollapsibles(); + this.select({ layerWithUuid: this._lastSelectedUuid }); + } + + updateLimits() { + if (!this.loaded) { + this.didLoad(_ => { + this.updateLimits(); + }); + return; + } + this._symbolCount.text(`${UIApplication.shared.symbolArt.root.numberOfSymbols}`); + this._symbolTotal.text(`${SymbolArt.maximumNumberOfSymbols}`); + this._containerCount.text(`${UIApplication.shared.symbolArt.root.containers.length}`); + this._containerTotal.text(`${SymbolArt.maximumNumberOfContainers}`); + } + + updateState() { + switch (ApplicationState.shared.viewMode) { + case ViewMode.symbolEditorMode: + case ViewMode.layerEditorMode: + this.isHidden = false; + break; + case ViewMode.helperImageMode: + default: + this.isHidden = true; + return; + } + } + + select({ layerWithUuid }) { + if (!this.loaded) { + this.didLoad(_ => { + this.select({ layerWithUuid: layerWithUuid }); + }); + return; + } + this._uiLayers.forEach(uiLayer => { + uiLayer.selected = uiLayer.identifier === layerWithUuid; + }); + this._lastSelectedUuid = layerWithUuid; + this.scroll({ toLayerWithUuid: layerWithUuid }); + } + + scroll({ toLayerWithUuid }) { + if (!this.loaded) { + this.didLoad(_ => { + this.scroll({ toLayerWithUuid: toLayerWithUuid }); + }); + return; + } + let uiLayer = this._uiLayers.filter(a => a.identifier === toLayerWithUuid)[0]; + if (uiLayer) { + uiLayer.didLoad(_ => { + uiLayer.view[0].scrollIntoView({ block: "nearest", inline: "nearest" }); + }); + } + } + +} diff --git a/src/components/ui/UIMenu.js b/src/components/ui/UIMenu.js index 6adf9e0..994e7d4 100644 --- a/src/components/ui/UIMenu.js +++ b/src/components/ui/UIMenu.js @@ -33,6 +33,8 @@ class UIMenu extends UIView { get viewPath() { return 'res/templates/menu.html' } + _tapHoldEnabled = true; + _collapsibleMenu = new UICollapsibleMenu(); _ellipsisButton = (() => { @@ -45,6 +47,21 @@ class UIMenu extends UIView { }); }); })(); + + _tapHoldToggleButton = (() => { + this.didLoad(_ => { + this._tapHoldToggleButton = this.view.find('#tapholdtogglebutton'); + this._tapHoldToggleButton.gestureRecognizer = new UITapGestureRecognizer({ + targetHtmlElement: this._tapHoldToggleButton[0], onTap: () => { + if (!this._tapHoldEnabled) { + ApplicationState.shared.interaction = InteractionType.enablingTapHoldFeature; + } else { + ApplicationState.shared.interaction = InteractionType.disablingTapHoldFeature; + } + } + }); + }); + })(); _undoButton = (() => { this.didLoad(_ => { @@ -89,6 +106,16 @@ class UIMenu extends UIView { ApplicationState.shared.add({ onChangeViewModeListener: () => { this.updateState(); + }, + onChangeInteractionListener: () => { + if (ApplicationState.shared.interaction === InteractionType.enablingTapHoldFeature) { + this._tapHoldEnabled = true; + this.updateState(); + } + if (ApplicationState.shared.interaction === InteractionType.disablingTapHoldFeature) { + this._tapHoldEnabled = false; + this.updateState(); + } } }); HistoryState.shared.add({ @@ -109,6 +136,7 @@ class UIMenu extends UIView { this._redoButton.css('opacity', HistoryState.shared.isAtMostRecentState ? '0.5' : '1'); this._symbolArtTypeLabel.text(UIApplication.shared.symbolArt.type === SymbolArtType.symbolArt ? 'SYMBOL ART' : 'TEAM FLAG'); this._symbolArtTypeLabel.css('opacity', window.innerWidth < 460 && ApplicationState.shared.viewMode === ViewMode.layerEditorMode ? '0' : ''); + this._tapHoldToggleButton.css('color', this._tapHoldEnabled ? 'white' : '#ff9e2c'); }); } diff --git a/src/components/ui/UIModalTextField.js b/src/components/ui/UIModalTextField.js index 19f97e9..bb9212f 100644 --- a/src/components/ui/UIModalTextField.js +++ b/src/components/ui/UIModalTextField.js @@ -90,6 +90,11 @@ class UIModalTextField extends UIView { // to prevent propagation } }); + this._textField.on('keyup', event => { + if (event.key === 'Enter' || event.keyCode === 13) { + this._close(); + } + }); }); })(); diff --git a/src/components/ui/UIView.js b/src/components/ui/UIView.js index f4c4dfb..015d8b9 100644 --- a/src/components/ui/UIView.js +++ b/src/components/ui/UIView.js @@ -114,5 +114,11 @@ class UIView extends UUID { }); }); } + + remove() { + this.didLoad(_ => { + this.view.remove(); + }); + } } diff --git a/src/model/Container.js b/src/model/Container.js index bf80243..e49f2a2 100644 --- a/src/model/Container.js +++ b/src/model/Container.js @@ -34,16 +34,31 @@ class Container extends Layer { static maximumDepth = 5; get origin() { - let containerOrigin = new Origin(); + let x = 0; + let y = 0; let origins = this.symbols.map(a => a.frame.origin); for (var index in origins) { let origin = origins[index]; - containerOrigin.x += origin.x; - containerOrigin.y += origin.y; + x += origin.x; + y += origin.y; + } + x /= origins.length; + y /= origins.length; + return new Origin({ x: x, y: y }); + } + set origin(value) { + if (!(value instanceof Origin)) return; + let currentOrigin = this.origin; + let dx = SymbolArt.scaling * Math.round((value.x - currentOrigin.x) / SymbolArt.scaling); + let dy = SymbolArt.scaling * Math.round((value.y - currentOrigin.y) / SymbolArt.scaling); + let frames = this.symbols.map(a => a.frame); + for (var index in frames) { + let origin = frames[index].origin; + frames[index].origin = new Origin({ + x: origin.x + SymbolArt.scaling * Math.round(dx / SymbolArt.scaling), + y: origin.y + SymbolArt.scaling * Math.round(dy / SymbolArt.scaling) + }); } - containerOrigin.x /= origins.length; - containerOrigin.y /= origins.length; - return containerOrigin; } get boundingBox() { @@ -223,6 +238,21 @@ class Container extends Layer { } } } + + reverseDepthFirstIterator(callback) { + for (var index = this._sublayers.length - 1; index >= 0; index--) { + let sublayer = this._sublayers[index]; + if (sublayer instanceof Symbol) { + let shouldBreak = callback(sublayer); + if (shouldBreak) return true; + } else if (sublayer instanceof Container) { + let shouldBreak = callback(sublayer); + if (shouldBreak) return true; + shouldBreak = sublayer.reverseDepthFirstIterator(callback); + if (shouldBreak) return true; + } + } + } contains({ sublayer = undefined } = {}) { if (typeof sublayer === 'undefined') diff --git a/src/model/InteractionType.js b/src/model/InteractionType.js index c7f22bf..371c269 100644 --- a/src/model/InteractionType.js +++ b/src/model/InteractionType.js @@ -49,6 +49,9 @@ class InteractionType { static reshapingHelperImageDidBegin = 15; static reshapingContainerWillBegin = 16; static reshapingContainerDidBegin = 17; + static willCloneCurrentSelection = 18; + static enablingTapHoldFeature = 19; + static disablingTapHoldFeature = 20; static valid({ rawValue }) { switch (rawValue) { @@ -70,6 +73,9 @@ class InteractionType { case InteractionType.reshapingHelperImageDidBegin: case InteractionType.reshapingContainerWillBegin: case InteractionType.reshapingContainerDidBegin: + case InteractionType.willCloneCurrentSelection: + case InteractionType.enablingTapHoldFeature: + case InteractionType.disablingTapHoldFeature: return true; default: return false; diff --git a/src/model/Symbol.js b/src/model/Symbol.js index 9f65ce8..94df2df 100644 --- a/src/model/Symbol.js +++ b/src/model/Symbol.js @@ -110,6 +110,7 @@ class Symbol extends Layer { clone({ retainUuid = true } = { }) { let clone = new Symbol({ + name: this.name, origin: new Origin({ x: this.frame.origin.x, y: this.frame.origin.y }), sizeOfDiagonalAC: this.frame.sizeOfDiagonalAC, sizeOfDiagonalBD: this.frame.sizeOfDiagonalBD, diff --git a/src/view/renderer/RendererPresenter.js b/src/view/renderer/RendererPresenter.js index 05df3a5..287f05a 100644 --- a/src/view/renderer/RendererPresenter.js +++ b/src/view/renderer/RendererPresenter.js @@ -123,12 +123,51 @@ class RendererPresenter extends InputDevice { ApplicationState.shared.trigger = TriggerType.none; this._renderer.helperImage.greenScreenEnabled = !this._renderer.helperImage.greenScreenEnabled; } + }, + onChangeInteractionListener: _ => { + if (ApplicationState.shared.interaction === InteractionType.enablingTapHoldFeature) { + ApplicationState.shared.interaction = InteractionType.none; + this.longTouchesWithoutDelay = false; + } + if (ApplicationState.shared.interaction === InteractionType.disablingTapHoldFeature) { + ApplicationState.shared.interaction = InteractionType.none; + this.longTouchesWithoutDelay = true; + } } }); } - + tapped(event) { - if (ApplicationState.shared.interaction !== InteractionType.none) return; + if (ApplicationState.shared.viewMode === ViewMode.symbolEditorMode + && ApplicationState.shared.interaction === InteractionType.willCloneCurrentSelection) { + let layerToClone = UIApplication.shared.symbolArt.findLayer({ withUuidString: this._renderer.selectionUuid }); + let worldPosition = this.worldVector(event); + let newLayer = null; + if (layerToClone instanceof Layer && layerToClone.parent instanceof Container) { + newLayer = layerToClone.clone({ retainUuid: false }); + let container = layerToClone.parent; + let index = container.indexOf({ sublayer: layerToClone }) + 1; + container.add({ sublayer: newLayer, atIndex: index }); + } else { + newLayer = new Symbol(); + let container = UIApplication.shared.symbolArt.root; + container.add({ sublayer: newLayer }); + } + if (newLayer instanceof Symbol) { + let x = newLayer.frame.origin.x + SymbolArt.scaling * Math.round((worldPosition.x - newLayer.frame.origin.x) / SymbolArt.scaling); + let y = newLayer.frame.origin.y + SymbolArt.scaling * Math.round((worldPosition.y - newLayer.frame.origin.y) / SymbolArt.scaling); + newLayer.frame.origin = new Origin({ x: x, y: y }); + } else if (newLayer instanceof Container) { + newLayer.origin = new Origin({ x: worldPosition.x, y: worldPosition.y }); + } + ApplicationState.shared.interaction = InteractionType.none; + this._renderer.updateWith({ symbolArt: UIApplication.shared.symbolArt }); + this._renderer.setSelection({ layer3D: Object.values(Layer3D.layersInUse).filter(a => a.layerUuid === newLayer.uuid)[0] }); + SymbolControls3D.shared.attach({ toSymbol3D: this._renderer.selectionGroup }); + ContainerControls3D.shared.attach({ toContainer3D: this._renderer.selectionGroup }); + HelperImageControls3D.shared.attach({ toHelperImage: null }); + HistoryState.shared.pushHistory({ data: UIApplication.shared.symbolArt.clone() }); + } else if (ApplicationState.shared.interaction !== InteractionType.none) return; if (ApplicationState.shared.viewMode === ViewMode.layerEditorMode) { this._renderer.touched({ screenPosition: this.normalizedVector(event), hitsLayers: true, hitsControls: false }); if (this._renderer.selectionUuid) { @@ -141,7 +180,7 @@ class RendererPresenter extends InputDevice { this._updateSymbolColorGuess(); } } - + touchBegan(event, count) { if (count > 1) return; this._initialEvent = event; @@ -912,10 +951,24 @@ class RendererPresenter extends InputDevice { } else if ((event.ctrlKey || event.metaKey) && (event.code === 'Minus' || event.keyCode === 189)) { event.preventDefault(); this._renderer.zoom *= 0.95; + } else { + if (!this.longTouchesWithoutDelay && (event.key === "Shift" || event.keyCode === 16)) { + ApplicationState.shared.interaction = InteractionType.disablingTapHoldFeature; + } else if (ApplicationState.shared.viewMode === ViewMode.symbolEditorMode && (event.key === "Control" || event.keyCode === 17) && ApplicationState.shared.interaction !== InteractionType.willCloneCurrentSelection) { + ApplicationState.shared.interaction = InteractionType.willCloneCurrentSelection; + } else if (ApplicationState.shared.interaction === InteractionType.willCloneCurrentSelection && !(event.key === "Control" || event.keyCode === 17)) { + ApplicationState.shared.interaction = InteractionType.none; + } } } pressedKey(event) { + if (ApplicationState.shared.interaction === InteractionType.willCloneCurrentSelection) { + ApplicationState.shared.interaction = InteractionType.none; + } + if (this.longTouchesWithoutDelay && (event.key === "Shift" || event.keyCode === 16)) { + ApplicationState.shared.interaction = InteractionType.enablingTapHoldFeature; + } if (!event || ApplicationState.shared.interaction !== InteractionType.none || this._renderer.hasOngoingAnimations) return; if (event.code === 'Space' || event.keyCode === 32) { ApplicationState.shared.trigger = TriggerType.none; @@ -934,6 +987,10 @@ class RendererPresenter extends InputDevice { } else if (((event.ctrlKey || event.metaKey) && (event.code === 'KeyY' || event.keyCode === 89)) || ((event.ctrlKey || event.metaKey) && event.shiftKey && (event.code === 'KeyZ' || event.keyCode === 90))) { HistoryState.shared.redo(); + } else if (event.code === 'ArrowUp' || event.keyCode === 38) { + this._selectNextLayer(); + } else if (event.code === 'ArrowDown' || event.keyCode === 40) { + this._selectPreviousLayer(); } } @@ -1119,6 +1176,7 @@ class RendererPresenter extends InputDevice { if (layer.name == text) return; layer.name = text; this._renderer.updateWith({ symbolArt: UIApplication.shared.symbolArt }); + HistoryState.shared.pushHistory({ data: UIApplication.shared.symbolArt.clone() }); } }); } @@ -1239,5 +1297,42 @@ class RendererPresenter extends InputDevice { } } } + + _selectNextLayer() { + let foundCurrentlySelected = false; + let layers = []; + UIApplication.shared.symbolArt.root.reverseDepthFirstIterator(layer => { + layers.push(layer); + }); + for (var index = layers.length - 1; index >= 0; index--) { + let layer = layers[index]; + if (foundCurrentlySelected) { + this._renderer.setSelection({ layer3D: Object.values(Layer3D.layersInUse).filter(a => a.layerUuid === layer.uuid)[0] }); + SymbolControls3D.shared.attach({ toSymbol3D: this._renderer.selectionGroup }); + ContainerControls3D.shared.attach({ toContainer3D: this._renderer.selectionGroup }); + HelperImageControls3D.shared.attach({ toHelperImage: null }); + break; + } + if (layer.uuid === this._renderer.selectionUuid) { + foundCurrentlySelected = true; + } + } + } + + _selectPreviousLayer() { + let foundCurrentlySelected = false; + UIApplication.shared.symbolArt.root.reverseDepthFirstIterator(layer => { + if (foundCurrentlySelected) { + this._renderer.setSelection({ layer3D: Object.values(Layer3D.layersInUse).filter(a => a.layerUuid === layer.uuid)[0] }); + SymbolControls3D.shared.attach({ toSymbol3D: this._renderer.selectionGroup }); + ContainerControls3D.shared.attach({ toContainer3D: this._renderer.selectionGroup }); + HelperImageControls3D.shared.attach({ toHelperImage: null }); + return true; + } + if (layer.uuid === this._renderer.selectionUuid) { + foundCurrentlySelected = true; + } + }); + } }