From fbb395021da5e8ba115d36dfd683f8576a74bf80 Mon Sep 17 00:00:00 2001 From: Benjamin Vray Date: Wed, 29 Mar 2023 14:58:29 +0200 Subject: [PATCH] [IMP] web_editor: allow to flip/rotate image shapes This commit adds the possibility to horizontally/vertically mirror and rotate the shape of an image. We've introduced a new sub-option for image shapes called "Transform". This new option is not available for certain shapes: - If the transform will have no effect (a vertically and horizontally symmetrical shape). - With some animations, if the animation is too complex for the system to work. - For "devices" shapes because they are too specific for the option to work. And anyway "devices" shapes are already available horizontally flipped in the list. task-3094258 Part-of: odoo/odoo#119197 --- .../components/media_dialog/media_dialog.js | 6 + .../static/src/js/editor/snippets.options.js | 137 ++++++++++++++++++ addons/web_editor/views/snippets.xml | 124 ++++++++-------- 3 files changed, 210 insertions(+), 57 deletions(-) diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index 23ed79131b8..7892964a065 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -174,6 +174,12 @@ export class MediaDialog extends Component { if (this.props.media.dataset.shapeColors) { element.dataset.shapeColors = this.props.media.dataset.shapeColors; } + if (this.props.media.dataset.shapeFlip) { + element.dataset.shapeFlip = this.props.media.dataset.shapeFlip; + } + if (this.props.media.dataset.shapeRotate) { + element.dataset.shapeRotate = this.props.media.dataset.shapeRotate; + } } } for (const otherTab of Object.keys(TABS).filter(key => key !== this.state.activeTab)) { diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 544b1669d46..c623fae0f5b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -6129,6 +6129,12 @@ registry.ImageTools = ImageHandlerOption.extend({ img.dataset.originalMimetype = img.dataset.mimetype; img.dataset.mimetype = 'image/svg+xml'; } + // When the user selects a shape, we remove the data attributes + // that are not compatible with this shape. + if (saveData && !this._isTransformableShape()) { + delete img.dataset.shapeFlip; + delete img.dataset.shapeRotate; + } } } else { // Re-applying the modifications and deleting the shapes @@ -6136,6 +6142,8 @@ registry.ImageTools = ImageHandlerOption.extend({ delete img.dataset.shape; delete img.dataset.shapeColors; delete img.dataset.fileName; + delete img.dataset.shapeFlip; + delete img.dataset.shapeRotate; if (saveData) { img.dataset.mimetype = img.dataset.originalMimetype; delete img.dataset.originalMimetype; @@ -6158,6 +6166,38 @@ registry.ImageTools = ImageHandlerOption.extend({ await this._applyShapeAndColors(true, newColors); img.classList.add('o_modified_image_to_save'); }, + /** + * Flips the image shape horizontally. + * + * @see this.selectClass for parameters + */ + async setImgShapeFlipX(previewMode, widgetValue, params) { + await this._setImgShapeFlip("x"); + }, + /** + * Flips the image shape vertically. + * + * @see this.selectClass for parameters + */ + async setImgShapeFlipY(previewMode, widgetValue, params) { + await this._setImgShapeFlip("y"); + }, + /** + * Rotates the image shape 90 degrees to the left. + * + * @see this.selectClass for parameters + */ + async setImgShapeRotateLeft(previewMode, widgetValue, params) { + await this._setImgShapeRotate(-90); + }, + /** + * Rotates the image shape 90 degrees to the right. + * + * @see this.selectClass for parameters + */ + async setImgShapeRotateRight(previewMode, widgetValue, params) { + await this._setImgShapeRotate(90); + }, //-------------------------------------------------------------------------- // Private @@ -6247,6 +6287,26 @@ registry.ImageTools = ImageHandlerOption.extend({ const initialImageWidth = img.naturalWidth; const svg = new DOMParser().parseFromString(svgText, 'image/svg+xml').documentElement; + + // Modifies the SVG according to the "flip" or/and "rotate" options. + const shapeFlip = img.dataset.shapeFlip || ""; + const shapeRotate = img.dataset.shapeRotate || 0; + if ((shapeFlip || shapeRotate) && this._isTransformableShape()) { + let shapeTransformValues = []; + if (shapeFlip) { // Possible values => "x", "y", "xy" + shapeTransformValues.push(`scale${shapeFlip === "x" ? "X" : shapeFlip === "y" ? "Y" : ""}(-1)`); + } + if (shapeRotate) { // Possible values => "90", "180", "270" + shapeTransformValues.push(`rotate(${shapeRotate}deg)`); + } + // "transform-origin: center;" does not work on "#filterPath". But + // since its dimension is 1px * 1px the following solution works. + const transformOrigin = "transform-origin: 0.5px 0.5px;"; + // Applies the transformation values to the path used to create a + // mask over the SVG image. + svg.querySelector("#filterPath").setAttribute("style", `transform: ${shapeTransformValues.join(" ")}; ${transformOrigin}`); + } + const svgAspectRatio = parseInt(svg.getAttribute('width')) / parseInt(svg.getAttribute('height')); // We will store the image in base64 inside the SVG. // applyModifications will return a dataURL with the current filters @@ -6356,6 +6416,10 @@ registry.ImageTools = ImageHandlerOption.extend({ const img = this._getImg(); return isImageSupportedForStyle(img) || this._isImageSupportedForProcessing(img); } + if (["img_shape_transform_flip_x_opt", "img_shape_transform_flip_y_opt", + "img_shape_transform_rotate_x_opt", "img_shape_transform_rotate_y_opt"].includes(params.name)) { + return this._isTransformableShape(); + } return this._super(...arguments); }, /** @@ -6389,6 +6453,14 @@ registry.ImageTools = ImageHandlerOption.extend({ const img = this._getImg(); return (img.dataset.shapeColors && img.dataset.shapeColors.split(';')[parseInt(params.colorId)]) || ''; } + case 'setImgShapeFlipX': { + const imgEl = this._getImg(); + return imgEl.dataset.shapeFlip?.includes("x") || ""; + } + case 'setImgShapeFlipY': { + const imgEl = this._getImg(); + return imgEl.dataset.shapeFlip?.includes("y") || ""; + } } return this._super(...arguments); }, @@ -6504,6 +6576,8 @@ registry.ImageTools = ImageHandlerOption.extend({ delete img.dataset.shapeColors; delete img.dataset.fileName; delete img.dataset.originalMimetype; + delete img.dataset.shapeFlip; + delete img.dataset.shapeRotate; return; } if (img.dataset.mimetype !== "image/svg+xml") { @@ -6533,6 +6607,69 @@ registry.ImageTools = ImageHandlerOption.extend({ } return this._super(...arguments); }, + /** + * Flips the image shape (vertically or/and horizontally). + * + * @private + * @param {string} flipValue image shape flip value + */ + async _setImgShapeFlip(flipValue) { + const imgEl = this._getImg(); + const currentFlipValue = imgEl.dataset.shapeFlip || ""; + const newFlipValue = currentFlipValue.includes(flipValue) + ? currentFlipValue.replace(flipValue, "") + : currentFlipValue + flipValue; + if (newFlipValue) { + imgEl.dataset.shapeFlip = newFlipValue === "yx" ? "xy" : newFlipValue; + } else { + delete imgEl.dataset.shapeFlip; + } + await this._applyShapeAndColors(true, imgEl.dataset.shapeColors?.split(";")); + imgEl.classList.add("o_modified_image_to_save"); + }, + /** + * Rotates the image shape 90 degrees. + * + * @private + * @param {integer} rotation rotation value + */ + async _setImgShapeRotate(rotation) { + const imgEl = this._getImg(); + const currentRotateValue = parseInt(imgEl.dataset.shapeRotate) || 0; + const newRotateValue = (currentRotateValue + rotation + 360) % 360; + if (newRotateValue) { + imgEl.dataset.shapeRotate = newRotateValue; + } else { + delete imgEl.dataset.shapeRotate; + } + await this._applyShapeAndColors(true, imgEl.dataset.shapeColors?.split(";")); + imgEl.classList.add("o_modified_image_to_save"); + }, + /** + * Checks if the shape is in the "devices" category. + * + * @private + * @returns {boolean} + */ + _isDeviceShape() { + const imgEl = this._getImg(); + const shapeName = imgEl.dataset.shape; + if (!shapeName) { + return false; + } + const shapeCategory = imgEl.dataset.shape.split("/")[1]; + return shapeCategory === "devices"; + }, + /** + * Checks if the shape is transformable. + * + * @private + * @returns {boolean} + */ + _isTransformableShape() { + const shapeImgWidget = this._requestUserValueWidgets("shape_img_opt")[0]; + return (shapeImgWidget && !shapeImgWidget.getMethodsParams().noTransform) && !this._isDeviceShape(); + }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml index 678e8174eb0..57eac2043c7 100644 --- a/addons/web_editor/views/snippets.xml +++ b/addons/web_editor/views/snippets.xml @@ -518,17 +518,17 @@
Geometrics - - - + + + - - + + @@ -549,18 +549,18 @@ Geometrics Rounded - - - - + + + + - - + + @@ -569,8 +569,8 @@ - - + + @@ -588,7 +588,7 @@ - + Composites @@ -611,67 +611,67 @@ Composition - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + Patterns - - - + + + - - - - - - - - - - + + + + + + + + + + Solids - - - - + + + + - - - - - - + + + + + + Specials - - - - - + + + + + @@ -719,6 +719,16 @@ + + + + + +