From 4af83eadc45438966308c651090656f8ba9c7aa4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 4 Jun 2021 15:04:09 +0200 Subject: [PATCH] :sparkles: Import shadows,blur,exports --- .../src/app/main/ui/shapes/custom_stroke.cljs | 22 +-- frontend/src/app/main/ui/shapes/export.cljs | 78 +++++++++ frontend/src/app/main/ui/shapes/shape.cljs | 52 +----- frontend/src/app/util/import/parser.cljc | 149 ++++++++++++------ 4 files changed, 201 insertions(+), 100 deletions(-) create mode 100644 frontend/src/app/main/ui/shapes/export.cljs diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 1e19f219fd..0fa42483c7 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -30,7 +30,7 @@ (let [clip-id (str "inner-stroke-" render-id) shape-id (str "stroke-shape-" render-id)] [:> "clipPath" #js {:id clip-id} - [:use {:href (str "#" shape-id)}]])) + [:use {:xlinkHref (str "#" shape-id)}]])) (mf/defc outer-stroke-mask [{:keys [shape render-id]}] @@ -38,10 +38,10 @@ shape-id (str "stroke-shape-" render-id) stroke-width (:stroke-width shape 0)] [:mask {:id stroke-mask-id} - [:use {:href (str "#" shape-id) + [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] - [:use {:href (str "#" shape-id) + [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "black"}}]])) (mf/defc stroke-defs @@ -84,13 +84,13 @@ (str/join ";"))] [:g.outer-stroke-shape - [:symbol + [:defs [:> elem-name (-> (obj/clone base-props) (obj/set! "id" shape-id) (obj/set! "data-style" style-str) (obj/without ["style"]))]] - [:use {:href (str "#" shape-id) + [:use {:xlinkHref (str "#" shape-id) :mask (str "url(#" stroke-mask-id ")") :style (-> (obj/get base-props "style") (obj/clone) @@ -98,7 +98,7 @@ (obj/without ["fill" "fillOpacity"]) (obj/set! "fill" "none"))}] - [:use {:href (str "#" shape-id) + [:use {:xlinkHref (str "#" shape-id) :style (-> (obj/get base-props "style") (obj/clone) (obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]])) @@ -121,14 +121,18 @@ clip-id (str "inner-stroke-" render-id) shape-id (str "stroke-shape-" render-id) + clip-path (str "url('#" clip-id "')") shape-props (-> base-props (add-props {:id shape-id - :transform nil - :clipPath (str "url('#" clip-id "')")}) + :transform nil}) (add-style {:strokeWidth (* stroke-width 2)}))] [:g.inner-stroke-shape {:transform transform} - [:> elem-name shape-props]])) + [:defs + [:> elem-name shape-props]] + + [:use {:xlinkHref (str "#" shape-id) + :clipPath clip-path}]])) ; The SVG standard does not implement yet the 'stroke-alignment' diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs new file mode 100644 index 0000000000..924ddc7eba --- /dev/null +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -0,0 +1,78 @@ +;; 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.main.ui.shapes.export + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.util.json :as json] + [app.util.object :as obj] + [rumext.alpha :as mf])) + +(defn add-data + "Adds as metadata properties that we cannot deduce from the exported SVG" + [props shape] + (let [add! + (fn [props attr val] + (let [ns-attr (str "penpot:" (-> attr d/name))] + (-> props + (obj/set! ns-attr val)))) + frame? (= :frame (:type shape)) + group? (= :group (:type shape)) + rect? (= :text (:type shape)) + text? (= :text (:type shape)) + mask? (and group? (:masked-group? shape))] + (-> props + (add! :name (-> shape :name)) + (add! :blocked (-> shape (:blocked false) str)) + (add! :hidden (-> shape (:hidden false) str)) + (add! :type (-> shape :type d/name)) + + (add! :stroke-style (-> shape (:stroke-style :none) d/name)) + (add! :stroke-alignment (-> shape (:stroke-alignment :center) d/name)) + + (add! :transform (-> shape (:transform (gmt/matrix)) str)) + (add! :transform-inverse (-> shape (:transform-inverse (gmt/matrix)) str)) + + (cond-> (and rect? (some? (:r1 shape))) + (-> (add! :r1 (-> shape (:r1 0) str)) + (add! :r2 (-> shape (:r2 0) str)) + (add! :r3 (-> shape (:r3 0) str)) + (add! :r4 (-> shape (:r4 0) str)))) + + (cond-> text? + (-> (add! :grow-type (-> shape :grow-type)) + (add! :content (-> shape :content json/encode)))) + + (cond-> mask? + (add! :masked-group "true"))))) + +(mf/defc export-data + [{:keys [shape]}] + (let [props (-> (obj/new) + (add-data shape))] + [:> "penpot:shape" props + (for [{:keys [style hidden color offset-x offset-y blur spread]} (:shadow shape)] + [:> "penpot:shadow" #js {:penpot:shadow-type (d/name style) + :penpot:hidden (str hidden) + :penpot:color (str (:color color)) + :penpot:opacity (str (:opacity color)) + :penpot:offset-x (str offset-x) + :penpot:offset-y (str offset-y) + :penpot:blur (str blur) + :penpot:spread (str spread)}]) + + (when (some? (:blur shape)) + (let [{:keys [type hidden value]} (:blur shape)] + [:> "penpot:blur" #js {:penpot:blur-type (d/name type) + :penpot:hidden (str hidden) + :penpot:value (str value)}])) + + (for [{:keys [scale suffix type]} (:exports shape)] + [:> "penpot:export" #js {:penpot:type (d/name type) + :penpot:suffix suffix + :penpot:scale (str scale)}])])) + diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 62ce224777..4d3f4fc686 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -8,57 +8,15 @@ (:require [app.common.data :as d] [app.common.uuid :as uuid] - [app.common.geom.matrix :as gmt] [app.main.ui.context :as muc] [app.main.ui.shapes.custom-stroke :as cs] [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.gradients :as grad] + [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.svg-defs :as defs] [app.util.object :as obj] - [rumext.alpha :as mf] - [app.util.json :as json])) - -(defn add-metadata - "Adds as metadata properties that we cannot deduce from the exported SVG" - [props shape] - (let [add! - (fn [props attr val] - (let [ns-attr (str "penpot:" (-> attr d/name))] - (-> props - (obj/set! ns-attr val)))) - frame? (= :frame (:type shape)) - group? (= :group (:type shape)) - rect? (= :text (:type shape)) - text? (= :text (:type shape)) - mask? (and group? (:masked-group? shape))] - (-> props - (add! :name (-> shape :name)) - (add! :blocked (-> shape (:blocked false) str)) - (add! :hidden (-> shape (:hidden false) str)) - (add! :type (-> shape :type d/name)) - - (add! :stroke-style (-> shape (:stroke-style :none) d/name)) - (add! :stroke-alignment (-> shape (:stroke-alignment :center) d/name)) - - (add! :transform (-> shape (:transform (gmt/matrix)) str)) - (add! :transform-inverse (-> shape (:transform-inverse (gmt/matrix)) str)) - - (cond-> (and rect? (some? (:r1 shape))) - (-> (add! :r1 (-> shape (:r1 0) str)) - (add! :r2 (-> shape (:r2 0) str)) - (add! :r3 (-> shape (:r3 0) str)) - (add! :r4 (-> shape (:r4 0) str)))) - - (cond-> text? - (-> (add! :grow-type (-> shape :grow-type)) - (add! :content (-> shape :content json/encode)))) - - (cond-> mask? - (add! :masked-group "true")) - - (cond-> frame? - (obj/set! "xmlns:penpot" "https://penpot.app/xmlns"))))) + [rumext.alpha :as mf])) (mf/defc shape-container {::mf/forward-ref true @@ -92,14 +50,14 @@ (obj/set! "width" width) (obj/set! "height" height) (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink") - (obj/set! "xmlns" "http://www.w3.org/2000/svg"))) - - (add-metadata shape)) + (obj/set! "xmlns" "http://www.w3.org/2000/svg") + (obj/set! "xmlns:penpot" "https://penpot.app/xmlns")))) wrapper-tag (if frame? "svg" "g")] [:& (mf/provider muc/render-ctx) {:value render-id} [:> wrapper-tag wrapper-props + [:& ed/export-data {:shape shape}] [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] diff --git a/frontend/src/app/util/import/parser.cljc b/frontend/src/app/util/import/parser.cljc index 3753c4af28..8fd551a6d4 100644 --- a/frontend/src/app/util/import/parser.cljc +++ b/frontend/src/app/util/import/parser.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] + [app.common.uuid :as uuid] [app.util.color :as uc] [app.util.json :as json] [app.util.path.parser :as upp] @@ -28,24 +29,29 @@ (and (vector? node) (= ::close (first node)))) +(defn get-data [node] + (->> node :content (d/seek #(= :penpot:shape (:tag %))))) + (defn get-type [node] (if (close? node) (second node) - (-> (get-in node [:attrs :penpot:type]) - (keyword)))) + (let [data (get-data node)] + (-> (get-in data [:attrs :penpot:type]) + (keyword))))) (defn shape? [node] (or (close? node) - (contains? (:attrs node) :penpot:type))) + (some? (get-data node)))) (defn get-meta ([m att] (get-meta m att identity)) ([m att val-fn] (let [ns-att (->> att d/name (str "penpot:") keyword) - val (get-in m [:attrs ns-att])] + val (or (get-in m [:attrs ns-att]) + (get-in (get-data m) [:attrs ns-att]))] (when val (val-fn val))))) (defn get-children @@ -87,7 +93,7 @@ (def search-data-node? #{:rect :image :path :text :circle}) -(defn get-shape-data +(defn get-svg-data [type node] (if (search-data-node? type) @@ -102,14 +108,14 @@ (def has-position? #{:frame :rect :image :text}) (defn parse-position - [props data] - (let [values (->> (select-keys data [:x :y :width :height]) + [props svg-data] + (let [values (->> (select-keys svg-data [:x :y :width :height]) (d/mapm (fn [_ val] (d/parse-double val))))] (d/merge props values))) (defn parse-circle - [props data] - (let [values (->> (select-keys data [:cx :cy :rx :ry]) + [props svg-data] + (let [values (->> (select-keys svg-data [:cx :cy :rx :ry]) (d/mapm (fn [_ val] (d/parse-double val))))] {:x (- (:cx values) (:rx values)) @@ -118,8 +124,8 @@ :height (* (:ry values) 2)})) (defn parse-path - [props data] - (let [content (upp/parse-path (:d data)) + [props svg-data] + (let [content (upp/parse-path (:d svg-data)) selrect (gsh/content->selrect content) points (gsh/rect->points selrect)] @@ -130,10 +136,12 @@ (def url-regex #"url\(#([^\)]*)\)") -(defn seek-node [id coll] +(defn seek-node + [id coll] (->> coll (d/seek #(= id (-> % :attrs :id))))) -(defn parse-stops [gradient-node] +(defn parse-stops + [gradient-node] (->> gradient-node (node-seq) (filter #(= :stop (:tag %))) @@ -166,23 +174,23 @@ :width (get-meta gradient-node :width d/parse-double))))) (defn add-position - [props type node data] + [props type node svg-data] (cond-> props (has-position? type) - (-> (parse-position data) + (-> (parse-position svg-data) (gsh/setup-selrect)) (= type :circle) - (-> (parse-circle data) + (-> (parse-circle svg-data) (gsh/setup-selrect)) (= type :path) - (parse-path data))) + (parse-path svg-data))) (defn add-fill - [props type node data] + [props type node svg-data] - (let [fill (:fill data)] + (let [fill (:fill svg-data)] (cond-> props (= fill "none") (assoc :fill-color nil @@ -195,22 +203,22 @@ (uc/hex? fill) (assoc :fill-color fill - :fill-opacity (-> data (:fill-opacity "1") d/parse-double))))) + :fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double))))) (defn add-stroke - [props type node data] + [props type node svg-data] (let [stroke-style (get-meta node :stroke-style keyword) stroke-alignment (get-meta node :stroke-alignment keyword) - stroke (:stroke data)] + stroke (:stroke svg-data)] (cond-> props :always (assoc :stroke-alignment stroke-alignment - :stroke-style stroke-style - :stroke-color (-> data (:stroke "#000000")) - :stroke-opacity (-> data (:stroke-opacity "1") d/parse-double) - :stroke-width (-> data (:stroke-width "0") d/parse-double)) + :stroke-style stroke-style + :stroke-color (-> svg-data (:stroke "#000000")) + :stroke-opacity (-> svg-data (:stroke-opacity "1") d/parse-double) + :stroke-width (-> svg-data (:stroke-width "0") d/parse-double)) (str/starts-with? stroke "url") (assoc :stroke-color-gradient (parse-gradient node stroke) @@ -221,12 +229,12 @@ (update :stroke-width / 2)))) (defn add-image-data - [props node data] + [props node] (-> props - (assoc-in [:metadata :id] (get-meta node :media-id)) - (assoc-in [:metadata :width] (get-meta node :media-width)) + (assoc-in [:metadata :id] (get-meta node :media-id)) + (assoc-in [:metadata :width] (get-meta node :media-width)) (assoc-in [:metadata :height] (get-meta node :media-height)) - (assoc-in [:metadata :mtype] (get-meta node :media-mtype)))) + (assoc-in [:metadata :mtype] (get-meta node :media-mtype)))) (defn add-text-data [props node] @@ -245,6 +253,65 @@ mask? (assoc :masked-group? true)))) +(defn parse-shadow [node] + {:id (uuid/next) + :style (get-meta node :shadow-type keyword) + :hidden (get-meta node :hidden str->bool) + :color {:color (get-meta node :color) + :opacity (get-meta node :opacity d/parse-double)} + :offset-x (get-meta node :offset-x d/parse-double) + :offset-y (get-meta node :offset-y d/parse-double) + :blur (get-meta node :blur d/parse-double) + :spread (get-meta node :spread d/parse-double)}) + +(defn parse-blur [node] + {:id (uuid/next) + :type (get-meta node :blur-type keyword) + :hidden (get-meta node :hidden str->bool) + :value (get-meta node :value d/parse-double)}) + +(defn parse-export [node] + {:type (get-meta node :type keyword) + :suffix (get-meta node :suffix) + :scale (get-meta node :scale d/parse-double)}) + +(defn extract-from-data [node tag parse-fn] + (let [shape-data (get-data node)] + (->> shape-data + (node-seq) + (filter #(= (:tag %) tag)) + (mapv parse-fn)))) + +(defn add-shadows + [props node] + (let [shadows (extract-from-data node :penpot:shadow parse-shadow)] + (cond-> props + (not (empty? shadows)) + (assoc :shadow shadows)))) + +(defn add-blur + [props node] + (let [blur (->> (extract-from-data node :penpot:blur parse-blur) (first))] + (cond-> props + (some? blur) + (assoc :blur blur)))) + +(defn add-exports + [props node] + (let [exports (extract-from-data node :penpot:export parse-export)] + (cond-> props + (not (empty? exports)) + (assoc :exports exports)))) + +(defn get-image-name + [node] + (get-in node [:attrs :penpot:name])) + +(defn get-image-data + [node] + (let [svg-data (get-svg-data :image node)] + (:xlink:href svg-data))) + (defn parse-data [type node] @@ -254,12 +321,15 @@ hidden (get-meta node :hidden str->bool) transform (get-meta node :transform gmt/str->matrix) transform-inverse (get-meta node :transform-inverse gmt/str->matrix) - data (get-shape-data type node)] + svg-data (get-svg-data type node)] (-> {} - (add-position type node data) - (add-fill type node data) - (add-stroke type node data) + (add-position type node svg-data) + (add-fill type node svg-data) + (add-stroke type node svg-data) + (add-shadows node) + (add-blur node) + (add-exports node) (assoc :name name) (assoc :blocked blocked) (assoc :hidden hidden) @@ -268,7 +338,7 @@ (add-group-data node)) (cond-> (= :image type) - (add-image-data node data)) + (add-image-data node)) (cond-> (= :text type) (add-text-data node)) @@ -278,12 +348,3 @@ (cond-> (some? transform-inverse) (assoc :transform-inverse transform-inverse)))))) - -(defn get-image-name - [node] - (get-in node [:attrs :penpot:name])) - -(defn get-image-data - [node] - (let [data (get-shape-data :image node)] - (:xlink:href data)))