diff --git a/CHANGES.md b/CHANGES.md index 5cc764495..cab75cb40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ ## 1.13.0-beta ### :boom: Breaking changes + +- We've changed the behaviour of the border-radius so it works as CSS that [has some limits](https://www.w3.org/TR/css-backgrounds-3/#corner-overlap). + ### :sparkles: New features - Exporting big files flow [Taiga #2218](https://tree.taiga.io/project/penpot/us/2218) @@ -49,6 +52,7 @@ - Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532) - Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057) - Fix problem when importing SVG's with uses with overriding properties [#Taiga 2884](https://tree.taiga.io/project/penpot/issue/2884) +- Fix inconsistency with radius in SVG an CSS [#1587](https://github.com/penpot/penpot/issues/1587) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 75fe17862..d60ffdc23 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -11,6 +11,7 @@ [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.constraints :as gct] + [app.common.geom.shapes.corners :as gsc] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] @@ -153,3 +154,7 @@ ;; Constraints (dm/export gct/default-constraints-h) (dm/export gct/default-constraints-v) + +;; Corners +(dm/export gsc/shape-corners-1) +(dm/export gsc/shape-corners-4) diff --git a/common/src/app/common/geom/shapes/corners.cljc b/common/src/app/common/geom/shapes/corners.cljc new file mode 100644 index 000000000..16c6d17cd --- /dev/null +++ b/common/src/app/common/geom/shapes/corners.cljc @@ -0,0 +1,46 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.geom.shapes.corners) + +(defn fix-radius + ;; https://www.w3.org/TR/css-backgrounds-3/#corner-overlap + ;; + ;; > Corner curves must not overlap: When the sum of any two adjacent border radii exceeds the size of the border box, + ;; > UAs must proportionally reduce the used values of all border radii until none of them overlap. + ;; + ;; > The algorithm for reducing radii is as follows: Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is + ;; > the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and + ;; > Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f. + ([width height r] + (let [f (min (/ width (* 2 r)) + (/ height (* 2 r)))] + (if (< f 1) + (* r f) + r))) + + ([width height r1 r2 r3 r4] + (let [f (min (/ width (+ r1 r2)) + (/ height (+ r2 r3)) + (/ width (+ r3 r4)) + (/ height (+ r4 r1)))] + (if (< f 1) + [(* r1 f) (* r2 f) (* r3 f) (* r4 f)] + [r1 r2 r3 r4])))) + +(defn shape-corners-1 + "Retrieve the effective value for the corner given a single value for corner." + [{:keys [width height rx] :as shape}] + (if (some? rx) + (fix-radius width height rx) + 0)) + +(defn shape-corners-4 + "Retrieve the effective value for the corner given four values for the corners." + [{:keys [width height r1 r2 r3 r4]}] + (if (and (some? r1) (some? r2) (some? r3) (some? r4)) + (fix-radius width height r1 r2 r3 r4) + [r1 r2 r3 r4])) diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 4a297f7f0..bb4e439c3 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -11,9 +11,11 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gsc] + [app.common.geom.shapes.corners :as gso] [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] - [app.common.path.commands :as pc])) + [app.common.path.commands :as pc] + [app.common.spec.radius :as ctr])) (def ^:const bezier-circle-c 0.551915024494) @@ -93,7 +95,7 @@ (defn circle->path "Creates the bezier curves to approximate a circle shape" - [x y width height] + [{:keys [x y width height]}] (let [mx (+ x (/ width 2)) my (+ y (/ height 2)) ex (+ x width) @@ -116,35 +118,50 @@ (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) +(defn draw-rounded-rect-path + ([x y width height r] + (draw-rounded-rect-path x y width height r r r r)) + + ([x y width height r1 r2 r3 r4] + (let [p1 (gpt/point x (+ y r1)) + p2 (gpt/point (+ x r1) y) + + p3 (gpt/point (+ width x (- r2)) y) + p4 (gpt/point (+ width x) (+ y r2)) + + p5 (gpt/point (+ width x) (+ height y (- r3))) + p6 (gpt/point (+ width x (- r3)) (+ height y)) + + p7 (gpt/point (+ x r4) (+ height y)) + p8 (gpt/point x (+ height y (- r4)))] + (-> [] + (conj (pc/make-move-to p1)) + (cond-> (not= p1 p2) + (conj (make-corner-arc p1 p2 :top-left r1))) + (conj (pc/make-line-to p3)) + (cond-> (not= p3 p4) + (conj (make-corner-arc p3 p4 :top-right r2))) + (conj (pc/make-line-to p5)) + (cond-> (not= p5 p6) + (conj (make-corner-arc p5 p6 :bottom-right r3))) + (conj (pc/make-line-to p7)) + (cond-> (not= p7 p8) + (conj (make-corner-arc p7 p8 :bottom-left r4))) + (conj (pc/make-line-to p1)))))) + (defn rect->path "Creates a bezier curve that approximates a rounded corner rectangle" - [x y width height r1 r2 r3 r4 rx] - (let [[r1 r2 r3 r4] (->> [r1 r2 r3 r4] (mapv #(or % rx 0))) - p1 (gpt/point x (+ y r1)) - p2 (gpt/point (+ x r1) y) + [{:keys [x y width height] :as shape}] + (case (ctr/radius-mode shape) + :radius-1 + (let [radius (gso/shape-corners-1 shape)] + (draw-rounded-rect-path x y width height radius)) - p3 (gpt/point (+ width x (- r2)) y) - p4 (gpt/point (+ width x) (+ y r2)) + :radius-4 + (let [[r1 r2 r3 r4] (gso/shape-corners-4 shape)] + (draw-rounded-rect-path x y width height r1 r2 r3 r4)) - p5 (gpt/point (+ width x) (+ height y (- r3))) - p6 (gpt/point (+ width x (- r3)) (+ height y)) - - p7 (gpt/point (+ x r4) (+ height y)) - p8 (gpt/point x (+ height y (- r4)))] - (-> [] - (conj (pc/make-move-to p1)) - (cond-> (not= p1 p2) - (conj (make-corner-arc p1 p2 :top-left r1))) - (conj (pc/make-line-to p3)) - (cond-> (not= p3 p4) - (conj (make-corner-arc p3 p4 :top-right r2))) - (conj (pc/make-line-to p5)) - (cond-> (not= p5 p6) - (conj (make-corner-arc p5 p6 :bottom-right r3))) - (conj (pc/make-line-to p7)) - (cond-> (not= p7 p8) - (conj (make-corner-arc p7 p8 :bottom-left r4))) - (conj (pc/make-line-to p1))))) + [])) (declare convert-to-path) @@ -192,9 +209,9 @@ "Transforms the given shape to a path" ([shape] (convert-to-path shape {})) - ([{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] + ([{:keys [type metadata] :as shape} objects] (assert (map? objects)) - (case (:type shape) + (case type :group (group-to-path shape objects) @@ -204,8 +221,8 @@ (:rect :circle :image :text) (let [new-content (case type - :circle (circle->path x y width height) - #_:else (rect->path x y width height r1 r2 r3 r4 rx)) + :circle (circle->path shape) + #_:else (rect->path shape)) ;; Apply the transforms that had the shape transform (:transform shape) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index d555f574e..dd76b8a6a 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -7,6 +7,8 @@ (ns app.main.ui.shapes.attrs (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] [app.common.spec.radius :as ctr] [app.common.spec.shape :refer [stroke-caps-line stroke-caps-marker]] [app.main.ui.context :as muc] @@ -26,58 +28,30 @@ (->> values (map #(+ % width)) (str/join ",")))) -(defn- truncate-side - [shape ra-attr rb-attr dimension-attr] - (let [ra (ra-attr shape) - rb (rb-attr shape) - dimension (dimension-attr shape)] - (if (<= (+ ra rb) dimension) - [ra rb] - [(/ (* ra dimension) (+ ra rb)) - (/ (* rb dimension) (+ ra rb))]))) -(defn- truncate-radius - [shape] - (let [[r-top-left r-top-right] - (truncate-side shape :r1 :r2 :width) - - [r-right-top r-right-bottom] - (truncate-side shape :r2 :r3 :height) - - [r-bottom-right r-bottom-left] - (truncate-side shape :r3 :r4 :width) - - [r-left-bottom r-left-top] - (truncate-side shape :r4 :r1 :height)] - - [(min r-top-left r-left-top) - (min r-top-right r-right-top) - (min r-right-bottom r-bottom-right) - (min r-bottom-left r-left-bottom)])) - -(defn add-border-radius [attrs shape] +(defn add-border-radius [attrs {:keys [x y width height] :as shape}] (case (ctr/radius-mode shape) - :radius-1 - (obj/merge! attrs #js {:rx (:rx shape 0) - :ry (:ry shape 0)}) + (let [radius (gsh/shape-corners-1 shape)] + (obj/merge! attrs #js {:rx radius :ry radius})) :radius-4 - (let [[r1 r2 r3 r4] (truncate-radius shape) - top (- (:width shape) r1 r2) - right (- (:height shape) r2 r3) - bottom (- (:width shape) r3 r4) - left (- (:height shape) r4 r1)] - (obj/merge! attrs #js {:d (str "M" (+ (:x shape) r1) "," (:y shape) " " - "h" top " " - "a" r2 "," r2 " 0 0 1 " r2 "," r2 " " - "v" right " " - "a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " " - "h" (- bottom) " " - "a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " " - "v" (- left) " " - "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " - "z")})) + (let [[r1 r2 r3 r4] (gsh/shape-corners-4 shape) + top (- width r1 r2) + right (- height r2 r3) + bottom (- width r3 r4) + left (- height r4 r1)] + (obj/merge! attrs #js {:d (dm/str + "M" (+ x r1) "," y " " + "h" top " " + "a" r2 "," r2 " 0 0 1 " r2 "," r2 " " + "v" right " " + "a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " " + "h" (- bottom) " " + "a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " " + "v" (- left) " " + "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " + "z")})) attrs)) (defn add-fill