mirror of
https://github.com/penpot/penpot.git
synced 2025-06-08 16:31:37 +02:00
Merge pull request #1623 from penpot/feat/svg-texts
Render Text as native SVG elements
This commit is contained in:
commit
73f5e7c2ef
45 changed files with 1386 additions and 729 deletions
|
@ -61,13 +61,13 @@
|
||||||
explain (us/pretty-explain data)]
|
explain (us/pretty-explain data)]
|
||||||
{:status 400
|
{:status 400
|
||||||
:body (-> data
|
:body (-> data
|
||||||
(dissoc ::s/problems)
|
(dissoc ::s/problems ::s/value)
|
||||||
(dissoc ::s/value)
|
|
||||||
(cond-> explain (assoc :explain explain)))}))
|
(cond-> explain (assoc :explain explain)))}))
|
||||||
|
|
||||||
(defmethod handle-exception :assertion
|
(defmethod handle-exception :assertion
|
||||||
[error request]
|
[error request]
|
||||||
(let [edata (ex-data error)]
|
(let [edata (ex-data error)
|
||||||
|
explain (us/pretty-explain edata)]
|
||||||
(l/error ::l/raw (ex-message error)
|
(l/error ::l/raw (ex-message error)
|
||||||
::l/context (get-error-context request error)
|
::l/context (get-error-context request error)
|
||||||
:cause error)
|
:cause error)
|
||||||
|
@ -75,7 +75,9 @@
|
||||||
{:status 500
|
{:status 500
|
||||||
:body {:type :server-error
|
:body {:type :server-error
|
||||||
:code :assertion
|
:code :assertion
|
||||||
:data (dissoc edata ::s/problems ::s/value ::s/spec)}}))
|
:data (-> edata
|
||||||
|
(dissoc ::s/problems ::s/value ::s/spec)
|
||||||
|
(cond-> explain (assoc :explain explain)))}}))
|
||||||
|
|
||||||
(defmethod handle-exception :not-found
|
(defmethod handle-exception :not-found
|
||||||
[err _]
|
[err _]
|
||||||
|
|
|
@ -190,3 +190,20 @@
|
||||||
(multiply mtx)
|
(multiply mtx)
|
||||||
(translate (gpt/negate pt)))
|
(translate (gpt/negate pt)))
|
||||||
mtx))
|
mtx))
|
||||||
|
|
||||||
|
(defn determinant
|
||||||
|
"Determinant for the affinity transform"
|
||||||
|
[{:keys [a b c d _ _]}]
|
||||||
|
(- (* a d) (* c b)))
|
||||||
|
|
||||||
|
(defn inverse
|
||||||
|
"Gets the inverse of the affinity transform `mtx`"
|
||||||
|
[{:keys [a b c d e f] :as mtx}]
|
||||||
|
(let [det (determinant mtx)
|
||||||
|
a' (/ d det)
|
||||||
|
b' (/ (- b) det)
|
||||||
|
c' (/ (- c) det)
|
||||||
|
d' (/ a det)
|
||||||
|
e' (/ (- (* c f) (* d e)) det)
|
||||||
|
f' (/ (- (* b e) (* a f)) det)]
|
||||||
|
(Matrix. a' b' c' d' e' f')))
|
||||||
|
|
|
@ -70,9 +70,11 @@
|
||||||
([points matrix]
|
([points matrix]
|
||||||
(transform-points points nil matrix))
|
(transform-points points nil matrix))
|
||||||
([points center matrix]
|
([points center matrix]
|
||||||
|
(if (some? matrix)
|
||||||
(let [prev (if center (gmt/translate-matrix center) (gmt/matrix))
|
(let [prev (if center (gmt/translate-matrix center) (gmt/matrix))
|
||||||
post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix))
|
post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix))
|
||||||
|
|
||||||
tr-point (fn [point]
|
tr-point (fn [point]
|
||||||
(gpt/transform point (gmt/multiply prev matrix post)))]
|
(gpt/transform point (gmt/multiply prev matrix post)))]
|
||||||
(mapv tr-point points))))
|
(mapv tr-point points))
|
||||||
|
points)))
|
||||||
|
|
|
@ -9,8 +9,10 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.geom.shapes.common :as gco]
|
||||||
[app.common.geom.shapes.path :as gpp]
|
[app.common.geom.shapes.path :as gpp]
|
||||||
[app.common.geom.shapes.rect :as gpr]
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
|
[app.common.geom.shapes.text :as gte]
|
||||||
[app.common.math :as mth]))
|
[app.common.math :as mth]))
|
||||||
|
|
||||||
(defn orientation
|
(defn orientation
|
||||||
|
@ -283,6 +285,23 @@
|
||||||
(is-point-inside-ellipse? (first rect-points) ellipse-data)
|
(is-point-inside-ellipse? (first rect-points) ellipse-data)
|
||||||
(intersects-lines-ellipse? rect-lines ellipse-data))))
|
(intersects-lines-ellipse? rect-lines ellipse-data))))
|
||||||
|
|
||||||
|
(defn overlaps-text?
|
||||||
|
[{:keys [position-data] :as shape} rect]
|
||||||
|
|
||||||
|
(if (and (some? position-data) (d/not-empty? position-data))
|
||||||
|
(let [center (gco/center-shape shape)
|
||||||
|
|
||||||
|
transform-rect
|
||||||
|
(fn [rect-points]
|
||||||
|
(gco/transform-points rect-points center (:transform shape)))]
|
||||||
|
|
||||||
|
(->> position-data
|
||||||
|
(map (comp transform-rect
|
||||||
|
gpr/rect->points
|
||||||
|
gte/position-data->rect))
|
||||||
|
(some #(overlaps-rect-points? rect %))))
|
||||||
|
(overlaps-rect-points? rect (:points shape))))
|
||||||
|
|
||||||
(defn overlaps?
|
(defn overlaps?
|
||||||
"General case to check for overlapping between shapes and a rectangle"
|
"General case to check for overlapping between shapes and a rectangle"
|
||||||
[shape rect]
|
[shape rect]
|
||||||
|
@ -291,14 +310,25 @@
|
||||||
(update :x - stroke-width)
|
(update :x - stroke-width)
|
||||||
(update :y - stroke-width)
|
(update :y - stroke-width)
|
||||||
(update :width + (* 2 stroke-width))
|
(update :width + (* 2 stroke-width))
|
||||||
(update :height + (* 2 stroke-width))
|
(update :height + (* 2 stroke-width)))]
|
||||||
)]
|
|
||||||
(or (not shape)
|
(or (not shape)
|
||||||
(let [path? (= :path (:type shape))
|
(let [path? (= :path (:type shape))
|
||||||
circle? (= :circle (:type shape))]
|
circle? (= :circle (:type shape))
|
||||||
|
text? (= :text (:type shape))]
|
||||||
|
(cond
|
||||||
|
path?
|
||||||
(and (overlaps-rect-points? rect (:points shape))
|
(and (overlaps-rect-points? rect (:points shape))
|
||||||
(or (not path?) (overlaps-path? shape rect))
|
(overlaps-path? shape rect))
|
||||||
(or (not circle?) (overlaps-ellipse? shape rect)))))))
|
|
||||||
|
circle?
|
||||||
|
(and (overlaps-rect-points? rect (:points shape))
|
||||||
|
(overlaps-ellipse? shape rect))
|
||||||
|
|
||||||
|
text?
|
||||||
|
(overlaps-text? shape rect)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(overlaps-rect-points? rect (:points shape)))))))
|
||||||
|
|
||||||
(defn has-point-rect?
|
(defn has-point-rect?
|
||||||
[rect point]
|
[rect point]
|
||||||
|
|
31
common/src/app/common/geom/shapes/text.cljc
Normal file
31
common/src/app/common/geom/shapes/text.cljc
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
;; 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.text
|
||||||
|
(:require
|
||||||
|
[app.common.geom.shapes.common :as gco]
|
||||||
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
|
[app.common.geom.shapes.transforms :as gtr]))
|
||||||
|
|
||||||
|
(defn position-data->rect
|
||||||
|
[{:keys [x y width height]}]
|
||||||
|
{:x x
|
||||||
|
:y (- y height)
|
||||||
|
:width width
|
||||||
|
:height height})
|
||||||
|
|
||||||
|
(defn position-data-points
|
||||||
|
[{:keys [position-data] :as shape}]
|
||||||
|
(let [points (->> position-data
|
||||||
|
(mapcat (comp gpr/rect->points position-data->rect)))
|
||||||
|
transform (gtr/transform-matrix shape)]
|
||||||
|
(gco/transform-points points transform)))
|
||||||
|
|
||||||
|
(defn position-data-bounding-box
|
||||||
|
[shape]
|
||||||
|
(gpr/points->selrect (position-data-points shape)))
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,14 @@
|
||||||
(->> points
|
(->> points
|
||||||
(mapv #(gpt/add % move-vec))))
|
(mapv #(gpt/add % move-vec))))
|
||||||
|
|
||||||
|
(defn move-position-data
|
||||||
|
[position-data dx dy]
|
||||||
|
|
||||||
|
(->> position-data
|
||||||
|
(mapv #(-> %
|
||||||
|
(update :x + dx)
|
||||||
|
(update :y + dy)))))
|
||||||
|
|
||||||
(defn move
|
(defn move
|
||||||
"Move the shape relatively to its current
|
"Move the shape relatively to its current
|
||||||
position applying the provided delta."
|
position applying the provided delta."
|
||||||
|
@ -52,6 +60,7 @@
|
||||||
(update :points move-points move-vec)
|
(update :points move-points move-vec)
|
||||||
(d/update-when :x + dx)
|
(d/update-when :x + dx)
|
||||||
(d/update-when :y + dy)
|
(d/update-when :y + dy)
|
||||||
|
(d/update-when :position-data move-position-data dx dy)
|
||||||
(cond-> (= :bool type) (update :bool-content gpa/move-content move-vec))
|
(cond-> (= :bool type) (update :bool-content gpa/move-content move-vec))
|
||||||
(cond-> (= :path type) (update :content gpa/move-content move-vec)))))
|
(cond-> (= :path type) (update :content gpa/move-content move-vec)))))
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,17 @@
|
||||||
:fill-color-ref-file
|
:fill-color-ref-file
|
||||||
:fill-color-gradient
|
:fill-color-gradient
|
||||||
|
|
||||||
|
:stroke-style
|
||||||
|
:stroke-alignment
|
||||||
|
:stroke-width
|
||||||
|
:stroke-color
|
||||||
|
:stroke-color-ref-id
|
||||||
|
:stroke-color-ref-file
|
||||||
|
:stroke-opacity
|
||||||
|
:stroke-color-gradient
|
||||||
|
:stroke-cap-start
|
||||||
|
:stroke-cap-end
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur
|
:blur
|
||||||
|
|
|
@ -202,12 +202,52 @@
|
||||||
(s/def :internal.shape.text/content
|
(s/def :internal.shape.text/content
|
||||||
(s/nilable
|
(s/nilable
|
||||||
(s/or :text-container
|
(s/or :text-container
|
||||||
(s/keys :req-un [:internal.shape.text/type
|
(s/keys :req-un [:internal.shape.text/type]
|
||||||
:internal.shape.text/children]
|
:opt-un [:internal.shape.text/key
|
||||||
:opt-un [:internal.shape.text/key])
|
:internal.shape.text/children])
|
||||||
:text-content
|
:text-content
|
||||||
(s/keys :req-un [:internal.shape.text/text]))))
|
(s/keys :req-un [:internal.shape.text/text]))))
|
||||||
|
|
||||||
|
(s/def :internal.shape.text/position-data
|
||||||
|
(s/coll-of :internal.shape.text/position-data-element
|
||||||
|
:kind vector?
|
||||||
|
:min-count 1))
|
||||||
|
|
||||||
|
(s/def :internal.shape.text/position-data-element
|
||||||
|
(s/keys :req-un [:internal.shape.text.position-data/x
|
||||||
|
:internal.shape.text.position-data/y
|
||||||
|
:internal.shape.text.position-data/width
|
||||||
|
:internal.shape.text.position-data/height]
|
||||||
|
:opt-un [:internal.shape.text.position-data/fill-color
|
||||||
|
:internal.shape.text.position-data/fill-opacity
|
||||||
|
:internal.shape.text.position-data/font-family
|
||||||
|
:internal.shape.text.position-data/font-size
|
||||||
|
:internal.shape.text.position-data/font-style
|
||||||
|
:internal.shape.text.position-data/font-weight
|
||||||
|
:internal.shape.text.position-data/rtl
|
||||||
|
:internal.shape.text.position-data/text
|
||||||
|
:internal.shape.text.position-data/text-decoration
|
||||||
|
:internal.shape.text.position-data/text-transform]
|
||||||
|
))
|
||||||
|
|
||||||
|
(s/def :internal.shape.text.position-data/x ::us/safe-number)
|
||||||
|
(s/def :internal.shape.text.position-data/y ::us/safe-number)
|
||||||
|
(s/def :internal.shape.text.position-data/width ::us/safe-number)
|
||||||
|
(s/def :internal.shape.text.position-data/height ::us/safe-number)
|
||||||
|
|
||||||
|
(s/def :internal.shape.text.position-data/fill-color ::fill-color)
|
||||||
|
(s/def :internal.shape.text.position-data/fill-opacity ::fill-opacity)
|
||||||
|
(s/def :internal.shape.text.position-data/fill-color-gradient ::fill-color-gradient)
|
||||||
|
|
||||||
|
(s/def :internal.shape.text.position-data/font-family string?)
|
||||||
|
(s/def :internal.shape.text.position-data/font-size string?)
|
||||||
|
(s/def :internal.shape.text.position-data/font-style string?)
|
||||||
|
(s/def :internal.shape.text.position-data/font-weight string?)
|
||||||
|
(s/def :internal.shape.text.position-data/rtl boolean?)
|
||||||
|
(s/def :internal.shape.text.position-data/text string?)
|
||||||
|
(s/def :internal.shape.text.position-data/text-decoration string?)
|
||||||
|
(s/def :internal.shape.text.position-data/text-transform string?)
|
||||||
|
|
||||||
(s/def :internal.shape.path/command keyword?)
|
(s/def :internal.shape.path/command keyword?)
|
||||||
(s/def :internal.shape.path/params
|
(s/def :internal.shape.path/params
|
||||||
(s/nilable (s/map-of keyword? any?)))
|
(s/nilable (s/map-of keyword? any?)))
|
||||||
|
@ -226,7 +266,8 @@
|
||||||
|
|
||||||
(defmethod shape-spec :text [_]
|
(defmethod shape-spec :text [_]
|
||||||
(s/and ::shape-attrs
|
(s/and ::shape-attrs
|
||||||
(s/keys :opt-un [:internal.shape.text/content])))
|
(s/keys :opt-un [:internal.shape.text/content
|
||||||
|
:internal.shape.text/position-data])))
|
||||||
|
|
||||||
(defmethod shape-spec :path [_]
|
(defmethod shape-spec :path [_]
|
||||||
(s/and ::shape-attrs
|
(s/and ::shape-attrs
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
:text-transform "none"
|
:text-transform "none"
|
||||||
:text-align "left"
|
:text-align "left"
|
||||||
:text-decoration "none"
|
:text-decoration "none"
|
||||||
:fill-color clr/black
|
:fills [{:fill-color clr/black
|
||||||
:fill-opacity 1})
|
:fill-opacity 1}]})
|
||||||
|
|
||||||
(def typography-fields
|
(def typography-fields
|
||||||
[:font-id
|
[:font-id
|
||||||
|
|
|
@ -311,7 +311,11 @@
|
||||||
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
||||||
nodes (process-text-nodes page)
|
nodes (process-text-nodes page)
|
||||||
nodes (d/index-by :id nodes)
|
nodes (d/index-by :id nodes)
|
||||||
result (replace-text-nodes xmldata nodes)]
|
result (replace-text-nodes xmldata nodes)
|
||||||
|
|
||||||
|
;; SVG standard don't allow the entity nbsp.   is equivalent but
|
||||||
|
;; compatible with SVG
|
||||||
|
result (str/replace result " " " ")]
|
||||||
;; (println "------- ORIGIN:")
|
;; (println "------- ORIGIN:")
|
||||||
;; (cljs.pprint/pprint (xml->clj xmldata))
|
;; (cljs.pprint/pprint (xml->clj xmldata))
|
||||||
;; (println "------- RESULT:")
|
;; (println "------- RESULT:")
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
|
|
||||||
.dashboard-buttons {
|
.dashboard-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
foreignObject {
|
.text-editor,
|
||||||
.text-editor,
|
.rich-text {
|
||||||
.rich-text {
|
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: sourcesanspro;
|
font-family: sourcesanspro;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
@ -14,9 +12,9 @@ foreignObject {
|
||||||
span {
|
span {
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-editor {
|
.text-editor {
|
||||||
.DraftEditor-root {
|
.DraftEditor-root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -40,23 +38,22 @@ foreignObject {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text .paragraphs {
|
.rich-text .paragraphs {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.align-top {
|
&.align-top {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.align-center {
|
&.align-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.align-bottom {
|
&.align-bottom {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,19 +113,14 @@
|
||||||
|
|
||||||
(defn transform-fill
|
(defn transform-fill
|
||||||
[state ids color transform]
|
[state ids color transform]
|
||||||
(let [page-id (:current-page-id state)
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
objects (wsh/lookup-page-objects state page-id)
|
|
||||||
|
|
||||||
is-text? #(= :text (:type (get objects %)))
|
is-text? #(= :text (:type (get objects %)))
|
||||||
text-ids (filter is-text? ids)
|
text-ids (filter is-text? ids)
|
||||||
shape-ids (filter (comp not is-text?) ids)
|
shape-ids (remove is-text? ids)
|
||||||
|
|
||||||
attrs (cond-> {:fill-color nil
|
|
||||||
:fill-color-gradient nil
|
|
||||||
:fill-color-ref-file nil
|
|
||||||
:fill-color-ref-id nil
|
|
||||||
:fill-opacity nil}
|
|
||||||
|
|
||||||
|
attrs
|
||||||
|
(cond-> {}
|
||||||
(contains? color :color)
|
(contains? color :color)
|
||||||
(assoc :fill-color (:color color))
|
(assoc :fill-color (:color color))
|
||||||
|
|
||||||
|
@ -139,39 +134,50 @@
|
||||||
(assoc :fill-color-gradient (:gradient color))
|
(assoc :fill-color-gradient (:gradient color))
|
||||||
|
|
||||||
(contains? color :opacity)
|
(contains? color :opacity)
|
||||||
(assoc :fill-opacity (:opacity color)))
|
(assoc :fill-opacity (:opacity color))
|
||||||
;; Not nil attrs
|
|
||||||
clean-attrs (d/without-nils attrs)]
|
:always
|
||||||
|
(d/without-nils))
|
||||||
|
|
||||||
|
transform-attrs #(transform % attrs)]
|
||||||
|
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids))
|
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
|
||||||
(rx/of (dch/update-shapes
|
(rx/of (dch/update-shapes shape-ids transform-attrs)))))
|
||||||
shape-ids
|
|
||||||
#(transform % clean-attrs))))))
|
|
||||||
|
|
||||||
(defn swap-fills [shape index new-index]
|
(defn swap-fills [shape index new-index]
|
||||||
(let [first (get-in shape [:fills index])
|
(let [first (get-in shape [:fills index])
|
||||||
second (get-in shape [:fills new-index])]
|
second (get-in shape [:fills new-index])]
|
||||||
(-> shape
|
(-> shape
|
||||||
(assoc-in [:fills index] second)
|
(assoc-in [:fills index] second)
|
||||||
(assoc-in [:fills new-index] first))
|
(assoc-in [:fills new-index] first))))
|
||||||
))
|
|
||||||
|
|
||||||
(defn reorder-fills
|
(defn reorder-fills
|
||||||
[ids index new-index]
|
[ids index new-index]
|
||||||
(ptk/reify ::reorder-fills
|
(ptk/reify ::reorder-fills
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(rx/of (dch/update-shapes
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
ids
|
|
||||||
#(swap-fills % index new-index))))))
|
is-text? #(= :text (:type (get objects %)))
|
||||||
|
text-ids (filter is-text? ids)
|
||||||
|
shape-ids (remove is-text? ids)
|
||||||
|
transform-attrs #(swap-fills % index new-index)]
|
||||||
|
|
||||||
|
(rx/concat
|
||||||
|
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
|
||||||
|
(rx/of (dch/update-shapes shape-ids transform-attrs)))))))
|
||||||
|
|
||||||
(defn change-fill
|
(defn change-fill
|
||||||
[ids color position]
|
[ids color position]
|
||||||
(ptk/reify ::change-fill
|
(ptk/reify ::change-fill
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [change (fn [shape attrs] (assoc-in shape [:fills position] (into {} attrs)))]
|
(let [change (fn [shape attrs]
|
||||||
|
(-> shape
|
||||||
|
(cond-> (not (contains? shape :fills))
|
||||||
|
(assoc :fills []))
|
||||||
|
(assoc-in [:fills position] (into {} attrs))))]
|
||||||
(transform-fill state ids color change)))))
|
(transform-fill state ids color change)))))
|
||||||
|
|
||||||
(defn change-fill-and-clear
|
(defn change-fill-and-clear
|
||||||
|
@ -187,7 +193,9 @@
|
||||||
(ptk/reify ::add-fill
|
(ptk/reify ::add-fill
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [add (fn [shape attrs] (assoc shape :fills (into [attrs] (:fills shape))))]
|
(let [add (fn [shape attrs]
|
||||||
|
(-> shape
|
||||||
|
(update :fills #(into [attrs] %))))]
|
||||||
(transform-fill state ids color add)))))
|
(transform-fill state ids color add)))))
|
||||||
|
|
||||||
(defn remove-fill
|
(defn remove-fill
|
||||||
|
|
|
@ -50,12 +50,14 @@
|
||||||
(update state :workspace-editor-state dissoc id)))))
|
(update state :workspace-editor-state dissoc id)))))
|
||||||
|
|
||||||
(defn finalize-editor-state
|
(defn finalize-editor-state
|
||||||
[{:keys [id] :as shape}]
|
[id]
|
||||||
(ptk/reify ::finalize-editor-state
|
(ptk/reify ::finalize-editor-state
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(when (dwc/initialized? state)
|
(when (dwc/initialized? state)
|
||||||
(let [content (-> (get-in state [:workspace-editor-state id])
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
|
shape (get objects id)
|
||||||
|
content (-> (get-in state [:workspace-editor-state id])
|
||||||
(ted/get-editor-current-content))]
|
(ted/get-editor-current-content))]
|
||||||
(if (ted/content-has-text? content)
|
(if (ted/content-has-text? content)
|
||||||
(let [content (d/merge (ted/export-content content)
|
(let [content (d/merge (ted/export-content content)
|
||||||
|
@ -78,8 +80,8 @@
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [text-state (some->> content ted/import-content)
|
(let [text-state (some->> content ted/import-content)
|
||||||
attrs (get-in state [:workspace-local :defaults :font])
|
attrs (d/merge txt/default-text-attrs
|
||||||
|
(get-in state [:workspace-local :defaults :font]))
|
||||||
editor (cond-> (ted/create-editor-state text-state decorator)
|
editor (cond-> (ted/create-editor-state text-state decorator)
|
||||||
(and (nil? content) (some? attrs))
|
(and (nil? content) (some? attrs))
|
||||||
(ted/update-editor-current-block-data attrs))]
|
(ted/update-editor-current-block-data attrs))]
|
||||||
|
@ -95,7 +97,7 @@
|
||||||
(rx/filter (ptk/type? ::rt/navigate) stream)
|
(rx/filter (ptk/type? ::rt/navigate) stream)
|
||||||
(rx/filter #(= ::finalize-editor-state %) stream))
|
(rx/filter #(= ::finalize-editor-state %) stream))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/map #(finalize-editor-state shape))))))
|
(rx/map #(finalize-editor-state id))))))
|
||||||
|
|
||||||
(defn select-all
|
(defn select-all
|
||||||
"Select all content of the current editor. When not editor found this
|
"Select all content of the current editor. When not editor found this
|
||||||
|
@ -115,13 +117,31 @@
|
||||||
|
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
|
||||||
|
(defn to-new-fills
|
||||||
|
[data]
|
||||||
|
[(d/without-nils (select-keys data [:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file]))])
|
||||||
|
|
||||||
(defn- shape-current-values
|
(defn- shape-current-values
|
||||||
[shape pred attrs]
|
[shape pred attrs]
|
||||||
(let [root (:content shape)
|
(let [root (:content shape)
|
||||||
nodes (->> (txt/node-seq pred root)
|
nodes (->> (txt/node-seq pred root)
|
||||||
(map #(if (txt/is-text-node? %)
|
(map (fn [node]
|
||||||
(merge txt/default-text-attrs %)
|
(if (txt/is-text-node? node)
|
||||||
%)))]
|
(let [fills
|
||||||
|
(cond
|
||||||
|
(or (some? (:fill-color node))
|
||||||
|
(some? (:fill-opacity node))
|
||||||
|
(some? (:fill-color-gradient node)))
|
||||||
|
(to-new-fills node)
|
||||||
|
|
||||||
|
(some? (:fills node))
|
||||||
|
(:fills node)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(:fills txt/default-text-attrs))]
|
||||||
|
(-> (merge txt/default-text-attrs node)
|
||||||
|
(assoc :fills fills)))
|
||||||
|
node))))]
|
||||||
(attrs/get-attrs-multi nodes attrs)))
|
(attrs/get-attrs-multi nodes attrs)))
|
||||||
|
|
||||||
(defn current-root-values
|
(defn current-root-values
|
||||||
|
@ -138,18 +158,21 @@
|
||||||
(defn current-text-values
|
(defn current-text-values
|
||||||
[{:keys [editor-state attrs shape]}]
|
[{:keys [editor-state attrs shape]}]
|
||||||
(if editor-state
|
(if editor-state
|
||||||
(-> (ted/get-editor-current-inline-styles editor-state)
|
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
||||||
(select-keys attrs))
|
(select-keys attrs))
|
||||||
|
result (if (empty? result) txt/default-text-attrs result)]
|
||||||
|
result)
|
||||||
(shape-current-values shape txt/is-text-node? attrs)))
|
(shape-current-values shape txt/is-text-node? attrs)))
|
||||||
|
|
||||||
|
|
||||||
;; --- TEXT EDITION IMPL
|
;; --- TEXT EDITION IMPL
|
||||||
|
|
||||||
(defn- update-shape
|
(defn- update-text-content
|
||||||
[shape pred-fn merge-fn attrs]
|
[shape pred-fn update-fn attrs]
|
||||||
(let [merge-attrs #(merge-fn % attrs)
|
(let [update-attrs #(update-fn % attrs)
|
||||||
transform #(txt/transform-nodes pred-fn merge-attrs %)]
|
transform #(txt/transform-nodes pred-fn update-attrs %)]
|
||||||
(update shape :content transform)))
|
(-> shape
|
||||||
|
(update :content transform))))
|
||||||
|
|
||||||
(defn update-root-attrs
|
(defn update-root-attrs
|
||||||
[{:keys [id attrs]}]
|
[{:keys [id attrs]}]
|
||||||
|
@ -159,7 +182,11 @@
|
||||||
(let [objects (wsh/lookup-page-objects state)
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
|
|
||||||
update-fn #(update-shape % txt/is-root-node? attrs/merge attrs)
|
update-fn
|
||||||
|
(fn [shape]
|
||||||
|
(if (some? (:content shape))
|
||||||
|
(update-text-content shape txt/is-root-node? attrs/merge attrs)
|
||||||
|
(assoc shape :content (attrs/merge {:type "root"} attrs))))
|
||||||
|
|
||||||
shape-ids (cond (cph/text-shape? shape) [id]
|
shape-ids (cond (cph/text-shape? shape) [id]
|
||||||
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
||||||
|
@ -186,7 +213,7 @@
|
||||||
node
|
node
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs)
|
update-fn #(update-text-content % txt/is-paragraph-node? merge-fn attrs)
|
||||||
shape-ids (cond
|
shape-ids (cond
|
||||||
(cph/text-shape? shape) [id]
|
(cph/text-shape? shape) [id]
|
||||||
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
||||||
|
@ -208,12 +235,57 @@
|
||||||
update-node? (fn [node]
|
update-node? (fn [node]
|
||||||
(or (txt/is-text-node? node)
|
(or (txt/is-text-node? node)
|
||||||
(txt/is-paragraph-node? node)))
|
(txt/is-paragraph-node? node)))
|
||||||
|
|
||||||
update-fn #(update-shape % update-node? attrs/merge attrs)
|
|
||||||
shape-ids (cond
|
shape-ids (cond
|
||||||
(cph/text-shape? shape) [id]
|
(cph/text-shape? shape) [id]
|
||||||
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
||||||
(rx/of (dch/update-shapes shape-ids update-fn)))))))
|
(rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? attrs/merge attrs))))))))
|
||||||
|
|
||||||
|
(defn migrate-node
|
||||||
|
[node]
|
||||||
|
(let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])]
|
||||||
|
(cond-> node
|
||||||
|
(d/not-empty? color-attrs)
|
||||||
|
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
|
||||||
|
(assoc :fills [color-attrs]))
|
||||||
|
|
||||||
|
(nil? (:fills node))
|
||||||
|
(assoc :fills (:fills txt/default-text-attrs)))))
|
||||||
|
|
||||||
|
(defn migrate-content
|
||||||
|
[content]
|
||||||
|
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content))
|
||||||
|
|
||||||
|
(defn update-text-with-function
|
||||||
|
[id update-node-fn]
|
||||||
|
(ptk/reify ::update-text-with-function
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(when (nil? (get-in state [:workspace-editor-state id]))
|
||||||
|
(let [objects (wsh/lookup-page-objects state)
|
||||||
|
shape (get objects id)
|
||||||
|
|
||||||
|
update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?)
|
||||||
|
|
||||||
|
shape-ids
|
||||||
|
(cond
|
||||||
|
(cph/text-shape? shape) [id]
|
||||||
|
(cph/group-shape? shape) (cph/get-children-ids objects id))
|
||||||
|
|
||||||
|
update-content
|
||||||
|
(fn [content]
|
||||||
|
(->> content
|
||||||
|
(migrate-content)
|
||||||
|
(txt/transform-nodes update-node? update-node-fn)))
|
||||||
|
|
||||||
|
update-shape
|
||||||
|
(fn [shape]
|
||||||
|
(d/update-when shape :content update-content))]
|
||||||
|
|
||||||
|
(rx/of (dch/update-shapes shape-ids update-shape)))))))
|
||||||
|
|
||||||
;; --- RESIZE UTILS
|
;; --- RESIZE UTILS
|
||||||
|
|
||||||
|
|
|
@ -185,6 +185,7 @@
|
||||||
:transform
|
:transform
|
||||||
:transform-inverse
|
:transform-inverse
|
||||||
:rotation
|
:rotation
|
||||||
|
:position-data
|
||||||
:flip-x
|
:flip-x
|
||||||
:flip-y]})
|
:flip-y]})
|
||||||
(clear-local-transform)
|
(clear-local-transform)
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
(def pointer-move (cursor-ref :pointer-move 0 0 10 42))
|
(def pointer-move (cursor-ref :pointer-move 0 0 10 42))
|
||||||
(def pointer-node (cursor-ref :pointer-node 0 0 10 32))
|
(def pointer-node (cursor-ref :pointer-node 0 0 10 32))
|
||||||
(def resize-alt (cursor-ref :resize-alt))
|
(def resize-alt (cursor-ref :resize-alt))
|
||||||
(def text (cursor-ref :text))
|
|
||||||
(def zoom (cursor-ref :zoom))
|
(def zoom (cursor-ref :zoom))
|
||||||
(def zoom-in (cursor-ref :zoom-in))
|
(def zoom-in (cursor-ref :zoom-in))
|
||||||
(def zoom-out (cursor-ref :zoom-out))
|
(def zoom-out (cursor-ref :zoom-out))
|
||||||
|
@ -40,6 +39,7 @@
|
||||||
(def resize-ns (cursor-fn :resize-h 90))
|
(def resize-ns (cursor-fn :resize-h 90))
|
||||||
(def resize-nwse (cursor-fn :resize-h 135))
|
(def resize-nwse (cursor-fn :resize-h 135))
|
||||||
(def rotate (cursor-fn :rotate 90))
|
(def rotate (cursor-fn :rotate 90))
|
||||||
|
(def text (cursor-fn :text 0))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
(def resize-ew-2 (cursor-fn :resize-h-2 0))
|
(def resize-ew-2 (cursor-fn :resize-h-2 0))
|
||||||
|
|
57
frontend/src/app/main/ui/hooks/mutable_observer.cljs
Normal file
57
frontend/src/app/main/ui/hooks/mutable_observer.cljs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
;; 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.hooks.mutable-observer
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.logging :as log]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(log/set-level! :warn)
|
||||||
|
|
||||||
|
(defn use-mutable-observer
|
||||||
|
[on-change]
|
||||||
|
|
||||||
|
(let [prev-obs-ref (mf/use-ref nil)
|
||||||
|
node-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
on-mutation
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps on-change)
|
||||||
|
(fn [mutations]
|
||||||
|
(let [mutations
|
||||||
|
(->> mutations
|
||||||
|
(remove #(= "transform" (.-attributeName ^js %))))]
|
||||||
|
(when (d/not-empty? mutations)
|
||||||
|
(on-change (mf/ref-val node-ref))))))
|
||||||
|
|
||||||
|
set-node
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps on-mutation)
|
||||||
|
(fn [^js node]
|
||||||
|
(when (and (some? node) (not= (mf/ref-val node-ref) node))
|
||||||
|
(mf/set-ref-val! node-ref node)
|
||||||
|
|
||||||
|
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||||
|
(.disconnect prev-obs)
|
||||||
|
(mf/set-ref-val! prev-obs-ref nil))
|
||||||
|
|
||||||
|
(when (some? node)
|
||||||
|
(let [options #js {:attributes true
|
||||||
|
:childList true
|
||||||
|
:subtree true
|
||||||
|
:characterData true}
|
||||||
|
mutation-obs (js/MutationObserver. on-mutation)]
|
||||||
|
(mf/set-ref-val! prev-obs-ref mutation-obs)
|
||||||
|
(.observe mutation-obs node options))))))]
|
||||||
|
|
||||||
|
(mf/with-effect
|
||||||
|
(fn []
|
||||||
|
(when-let [^js prev-obs (mf/ref-val prev-obs-ref)]
|
||||||
|
(.disconnect prev-obs)
|
||||||
|
(mf/set-ref-val! prev-obs-ref nil))))
|
||||||
|
|
||||||
|
[node-ref set-node]))
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main.ui.render
|
(ns app.main.ui.render
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
[app.main.ui.shapes.embed :as embed]
|
[app.main.ui.shapes.embed :as embed]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
|
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -72,6 +74,8 @@
|
||||||
(:hide-fill-on-export object)
|
(:hide-fill-on-export object)
|
||||||
(assoc :fills []))
|
(assoc :fills []))
|
||||||
|
|
||||||
|
all-children (cph/get-children objects object-id)
|
||||||
|
|
||||||
{:keys [x y width height] :as bs} (calc-bounds object objects)
|
{:keys [x y width height] :as bs} (calc-bounds object objects)
|
||||||
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
|
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
|
||||||
|
|
||||||
|
@ -89,10 +93,11 @@
|
||||||
(mf/with-memo [objects]
|
(mf/with-memo [objects]
|
||||||
(render/shape-wrapper-factory objects))
|
(render/shape-wrapper-factory objects))
|
||||||
|
|
||||||
text-shapes
|
is-text? (fn [shape] (= :text (:type shape)))
|
||||||
(->> objects
|
|
||||||
(filter (fn [[_ shape]] (= :text (:type shape))))
|
text-shapes (sequence (comp (map second) (filter is-text?)) objects)
|
||||||
(mapv second))]
|
|
||||||
|
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
|
||||||
|
|
||||||
(mf/with-effect [width height]
|
(mf/with-effect [width height]
|
||||||
(dom/set-page-style {:size (str (mth/ceil width) "px "
|
(dom/set-page-style {:size (str (mth/ceil width) "px "
|
||||||
|
@ -110,6 +115,8 @@
|
||||||
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
|
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
|
||||||
:style {:-webkit-print-color-adjust :exact}}
|
:style {:-webkit-print-color-adjust :exact}}
|
||||||
|
|
||||||
|
[:& ff/fontfaces-style {:shapes all-children}]
|
||||||
|
|
||||||
(case (:type object)
|
(case (:type object)
|
||||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||||
:group [:> shape-container {:shape object}
|
:group [:> shape-container {:shape object}
|
||||||
|
|
|
@ -80,16 +80,20 @@
|
||||||
"z")}))
|
"z")}))
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
(defn add-fill [attrs shape render-id index]
|
(defn add-fill
|
||||||
(let [
|
([attrs shape render-id]
|
||||||
fill-attrs (cond
|
(add-fill attrs shape render-id nil))
|
||||||
|
|
||||||
|
([attrs shape render-id index]
|
||||||
|
(let [fill-attrs
|
||||||
|
(cond
|
||||||
(contains? shape :fill-image)
|
(contains? shape :fill-image)
|
||||||
(let [fill-image-id (str "fill-image-" render-id)]
|
(let [fill-image-id (str "fill-image-" render-id)]
|
||||||
{:fill (str/format "url(#%s)" fill-image-id)})
|
{:fill (str "url(#" fill-image-id ")")})
|
||||||
|
|
||||||
(contains? shape :fill-color-gradient)
|
(contains? shape :fill-color-gradient)
|
||||||
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)]
|
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))]
|
||||||
{:fill (str/format "url(#%s)" fill-color-gradient-id)})
|
{:fill (str "url(#" fill-color-gradient-id ")")})
|
||||||
|
|
||||||
(contains? shape :fill-color)
|
(contains? shape :fill-color)
|
||||||
{:fill (:fill-color shape)}
|
{:fill (:fill-color shape)}
|
||||||
|
@ -107,7 +111,7 @@
|
||||||
(contains? shape :fill-opacity)
|
(contains? shape :fill-opacity)
|
||||||
(assoc :fillOpacity (:fill-opacity shape)))]
|
(assoc :fillOpacity (:fill-opacity shape)))]
|
||||||
|
|
||||||
(obj/merge! attrs (clj->js fill-attrs))))
|
(obj/merge! attrs (clj->js fill-attrs)))))
|
||||||
|
|
||||||
(defn add-stroke [attrs shape render-id]
|
(defn add-stroke [attrs shape render-id]
|
||||||
(let [stroke-style (:stroke-style shape :none)
|
(let [stroke-style (:stroke-style shape :none)
|
||||||
|
@ -189,9 +193,8 @@
|
||||||
(let [svg-defs (:svg-defs shape {})
|
(let [svg-defs (:svg-defs shape {})
|
||||||
svg-attrs (:svg-attrs shape {})
|
svg-attrs (:svg-attrs shape {})
|
||||||
|
|
||||||
[svg-attrs svg-styles] (mf/use-memo
|
[svg-attrs svg-styles]
|
||||||
(mf/deps render-id svg-defs svg-attrs)
|
(extract-svg-attrs render-id svg-defs svg-attrs)
|
||||||
#(extract-svg-attrs render-id svg-defs svg-attrs))
|
|
||||||
|
|
||||||
styles (-> (obj/get props "style" (obj/new))
|
styles (-> (obj/get props "style" (obj/new))
|
||||||
(obj/merge! svg-styles)
|
(obj/merge! svg-styles)
|
||||||
|
@ -202,7 +205,7 @@
|
||||||
(= :image (:type shape))
|
(= :image (:type shape))
|
||||||
(> (count (:fills shape)) 1)
|
(> (count (:fills shape)) 1)
|
||||||
(some #(some? (:fill-color-gradient %)) (:fills shape)))
|
(some #(some? (:fill-color-gradient %)) (:fills shape)))
|
||||||
(obj/set! styles "fill" (str "url(#fill-" render-id ")"))
|
(obj/set! styles "fill" (str "url(#fill-0-" render-id ")"))
|
||||||
|
|
||||||
;; imported svgs can have fill and fill-opacity attributes
|
;; imported svgs can have fill and fill-opacity attributes
|
||||||
(obj/contains? svg-styles "fill")
|
(obj/contains? svg-styles "fill")
|
||||||
|
@ -224,9 +227,8 @@
|
||||||
(add-style-attrs shape)))
|
(add-style-attrs shape)))
|
||||||
|
|
||||||
(defn extract-fill-attrs
|
(defn extract-fill-attrs
|
||||||
[shape index]
|
[shape render-id index]
|
||||||
(let [render-id (mf/use-ctx muc/render-ctx)
|
(let [fill-styles (-> (obj/get shape "style" (obj/new))
|
||||||
fill-styles (-> (obj/get shape "style" (obj/new))
|
|
||||||
(add-fill shape render-id index))]
|
(add-fill shape render-id index))]
|
||||||
(-> (obj/new)
|
(-> (obj/new)
|
||||||
(obj/set! "style" fill-styles))))
|
(obj/set! "style" fill-styles))))
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.custom-stroke
|
(ns app.main.ui.shapes.custom-stroke
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
@ -25,37 +24,29 @@
|
||||||
(-> props (obj/merge #js {:style style}))))
|
(-> props (obj/merge #js {:style style}))))
|
||||||
|
|
||||||
(mf/defc inner-stroke-clip-path
|
(mf/defc inner-stroke-clip-path
|
||||||
[{:keys [render-id]}]
|
[{:keys [shape render-id index]}]
|
||||||
(let [clip-id (str "inner-stroke-" render-id)
|
(let [suffix (if index (str "-" index) "")
|
||||||
shape-id (str "stroke-shape-" render-id)]
|
clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix)
|
||||||
|
shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)]
|
||||||
[:> "clipPath" #js {:id clip-id}
|
[:> "clipPath" #js {:id clip-id}
|
||||||
[:use {:xlinkHref (str "#" shape-id)}]]))
|
[:use {:xlinkHref (str "#" shape-id)}]]))
|
||||||
|
|
||||||
(mf/defc outer-stroke-mask
|
(mf/defc outer-stroke-mask
|
||||||
[{:keys [shape render-id]}]
|
[{:keys [shape render-id index]}]
|
||||||
(let [stroke-mask-id (str "outer-stroke-" render-id)
|
(let [suffix (if index (str "-" index) "")
|
||||||
shape-id (str "stroke-shape-" render-id)
|
stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix)
|
||||||
|
shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)
|
||||||
stroke-width (case (:stroke-alignment shape :center)
|
stroke-width (case (:stroke-alignment shape :center)
|
||||||
:center (/ (:stroke-width shape 0) 2)
|
:center (/ (:stroke-width shape 0) 2)
|
||||||
:outer (:stroke-width shape 0)
|
:outer (:stroke-width shape 0)
|
||||||
0)
|
0)]
|
||||||
margin (gsh/shape-stroke-margin shape stroke-width)
|
[:mask {:id stroke-mask-id}
|
||||||
bounding-box (-> (gsh/points->selrect (:points shape))
|
|
||||||
(update :x - (+ stroke-width margin))
|
|
||||||
(update :y - (+ stroke-width margin))
|
|
||||||
(update :width + (* 2 (+ stroke-width margin)))
|
|
||||||
(update :height + (* 2 (+ stroke-width margin))))]
|
|
||||||
[:mask {:id stroke-mask-id
|
|
||||||
:x (:x bounding-box)
|
|
||||||
:y (:y bounding-box)
|
|
||||||
:width (:width bounding-box)
|
|
||||||
:height (:height bounding-box)
|
|
||||||
:maskUnits "userSpaceOnUse"}
|
|
||||||
[:use {:xlinkHref (str "#" shape-id)
|
[:use {:xlinkHref (str "#" shape-id)
|
||||||
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
|
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
|
||||||
|
|
||||||
[:use {:xlinkHref (str "#" shape-id)
|
[:use {:xlinkHref (str "#" shape-id)
|
||||||
:style #js {:fill "black"}}]]))
|
:style #js {:fill "black"
|
||||||
|
:stroke "none"}}]]))
|
||||||
|
|
||||||
(mf/defc cap-markers
|
(mf/defc cap-markers
|
||||||
[{:keys [shape render-id]}]
|
[{:keys [shape render-id]}]
|
||||||
|
@ -160,7 +151,7 @@
|
||||||
[:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])]))
|
[:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])]))
|
||||||
|
|
||||||
(mf/defc stroke-defs
|
(mf/defc stroke-defs
|
||||||
[{:keys [shape render-id]}]
|
[{:keys [shape render-id index]}]
|
||||||
|
|
||||||
(let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))]
|
(let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))]
|
||||||
(cond
|
(cond
|
||||||
|
@ -168,18 +159,21 @@
|
||||||
(= :inner (:stroke-alignment shape :center))
|
(= :inner (:stroke-alignment shape :center))
|
||||||
(> (:stroke-width shape 0) 0))
|
(> (:stroke-width shape 0) 0))
|
||||||
[:& inner-stroke-clip-path {:shape shape
|
[:& inner-stroke-clip-path {:shape shape
|
||||||
:render-id render-id}]
|
:render-id render-id
|
||||||
|
:index index}]
|
||||||
|
|
||||||
(and (not open-path?)
|
(and (not open-path?)
|
||||||
(= :outer (:stroke-alignment shape :center))
|
(= :outer (:stroke-alignment shape :center))
|
||||||
(> (:stroke-width shape 0) 0))
|
(> (:stroke-width shape 0) 0))
|
||||||
[:& outer-stroke-mask {:shape shape
|
[:& outer-stroke-mask {:shape shape
|
||||||
:render-id render-id}]
|
:render-id render-id
|
||||||
|
:index index}]
|
||||||
|
|
||||||
(or (some? (:stroke-cap-start shape))
|
(or (some? (:stroke-cap-start shape))
|
||||||
(some? (:stroke-cap-end shape)))
|
(some? (:stroke-cap-end shape)))
|
||||||
[:& cap-markers {:shape shape
|
[:& cap-markers {:shape shape
|
||||||
:render-id render-id}])))
|
:render-id render-id
|
||||||
|
:index index}])))
|
||||||
|
|
||||||
;; Outer alignment: display the shape in two layers. One
|
;; Outer alignment: display the shape in two layers. One
|
||||||
;; without stroke (only fill), and another one only with stroke
|
;; without stroke (only fill), and another one only with stroke
|
||||||
|
@ -194,36 +188,37 @@
|
||||||
child (obj/get props "children")
|
child (obj/get props "children")
|
||||||
base-props (obj/get child "props")
|
base-props (obj/get child "props")
|
||||||
elem-name (obj/get child "type")
|
elem-name (obj/get child "type")
|
||||||
stroke-mask-id (str "outer-stroke-" render-id)
|
shape (obj/get props "shape")
|
||||||
shape-id (str "stroke-shape-" render-id)
|
index (obj/get props "index")
|
||||||
|
stroke-width (:stroke-width shape)
|
||||||
|
|
||||||
style-str (->> (obj/get base-props "style")
|
suffix (if index (str "-" index) "")
|
||||||
(js->clj)
|
stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix)
|
||||||
(mapv (fn [[k v]]
|
shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)]
|
||||||
(-> (d/name k)
|
|
||||||
(str/kebab)
|
|
||||||
(str ":" v))))
|
|
||||||
(str/join ";"))]
|
|
||||||
|
|
||||||
[:g.outer-stroke-shape
|
[:g.outer-stroke-shape
|
||||||
[:defs
|
[:defs
|
||||||
|
[:& stroke-defs {:shape shape :render-id render-id :index index}]
|
||||||
[:> elem-name (-> (obj/clone base-props)
|
[:> elem-name (-> (obj/clone base-props)
|
||||||
(obj/set! "id" shape-id)
|
(obj/set! "id" shape-id)
|
||||||
(obj/set! "data-style" style-str)
|
(obj/set!
|
||||||
(obj/without ["style"]))]]
|
"style"
|
||||||
|
(-> (obj/get base-props "style")
|
||||||
|
(obj/clone)
|
||||||
|
(obj/without ["fill" "fillOpacity" "stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))))]]
|
||||||
|
|
||||||
[:use {:xlinkHref (str "#" shape-id)
|
[:use {:xlinkHref (str "#" shape-id)
|
||||||
:mask (str "url(#" stroke-mask-id ")")
|
:mask (str "url(#" stroke-mask-id ")")
|
||||||
:style (-> (obj/get base-props "style")
|
:style (-> (obj/get base-props "style")
|
||||||
(obj/clone)
|
(obj/clone)
|
||||||
(obj/update! "strokeWidth" * 2)
|
(obj/set! "strokeWidth" (* stroke-width 2))
|
||||||
(obj/without ["fill" "fillOpacity"])
|
(obj/without ["fill" "fillOpacity"])
|
||||||
(obj/set! "fill" "none"))}]
|
(obj/set! "fill" "none"))}]
|
||||||
|
|
||||||
[:use {:xlinkHref (str "#" shape-id)
|
[:use {:xlinkHref (str "#" shape-id)
|
||||||
:style (-> (obj/get base-props "style")
|
:style (-> (obj/get base-props "style")
|
||||||
(obj/clone)
|
(obj/clone)
|
||||||
(obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]]))
|
(obj/set! "stroke" "none"))}]]))
|
||||||
|
|
||||||
|
|
||||||
;; Inner alignment: display the shape with double width stroke,
|
;; Inner alignment: display the shape with double width stroke,
|
||||||
|
@ -236,12 +231,14 @@
|
||||||
base-props (obj/get child "props")
|
base-props (obj/get child "props")
|
||||||
elem-name (obj/get child "type")
|
elem-name (obj/get child "type")
|
||||||
shape (obj/get props "shape")
|
shape (obj/get props "shape")
|
||||||
|
index (obj/get props "index")
|
||||||
transform (obj/get base-props "transform")
|
transform (obj/get base-props "transform")
|
||||||
|
|
||||||
stroke-width (:stroke-width shape 0)
|
stroke-width (:stroke-width shape 0)
|
||||||
|
|
||||||
clip-id (str "inner-stroke-" render-id)
|
suffix (if index (str "-" index) "")
|
||||||
shape-id (str "stroke-shape-" render-id)
|
clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix)
|
||||||
|
shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)
|
||||||
|
|
||||||
clip-path (str "url('#" clip-id "')")
|
clip-path (str "url('#" clip-id "')")
|
||||||
shape-props (-> base-props
|
shape-props (-> base-props
|
||||||
|
@ -251,12 +248,12 @@
|
||||||
|
|
||||||
[:g.inner-stroke-shape {:transform transform}
|
[:g.inner-stroke-shape {:transform transform}
|
||||||
[:defs
|
[:defs
|
||||||
|
[:& stroke-defs {:shape shape :render-id render-id :index index}]
|
||||||
[:> elem-name shape-props]]
|
[:> elem-name shape-props]]
|
||||||
|
|
||||||
[:use {:xlinkHref (str "#" shape-id)
|
[:use {:xlinkHref (str "#" shape-id)
|
||||||
:clipPath clip-path}]]))
|
:clipPath clip-path}]]))
|
||||||
|
|
||||||
|
|
||||||
; The SVG standard does not implement yet the 'stroke-alignment'
|
; The SVG standard does not implement yet the 'stroke-alignment'
|
||||||
; attribute, to define the position of the stroke relative to the
|
; attribute, to define the position of the stroke relative to the
|
||||||
; stroke axis (inner, center, outer). Here we implement a patch to be
|
; stroke axis (inner, center, outer). Here we implement a patch to be
|
||||||
|
@ -265,8 +262,10 @@
|
||||||
(mf/defc shape-custom-stroke
|
(mf/defc shape-custom-stroke
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
|
|
||||||
(let [child (obj/get props "children")
|
(let [child (obj/get props "children")
|
||||||
shape (obj/get props "shape")
|
shape (obj/get props "shape")
|
||||||
|
index (obj/get props "index")
|
||||||
stroke-width (:stroke-width shape 0)
|
stroke-width (:stroke-width shape 0)
|
||||||
stroke-style (:stroke-style shape :none)
|
stroke-style (:stroke-style shape :none)
|
||||||
stroke-position (:stroke-alignment shape :center)
|
stroke-position (:stroke-alignment shape :center)
|
||||||
|
@ -279,11 +278,11 @@
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(and has-stroke? inner? closed?)
|
(and has-stroke? inner? closed?)
|
||||||
[:& inner-stroke {:shape shape}
|
[:& inner-stroke {:shape shape :index index}
|
||||||
child]
|
child]
|
||||||
|
|
||||||
(and has-stroke? outer? closed?)
|
(and has-stroke? outer? closed?)
|
||||||
[:& outer-stroke {:shape shape}
|
[:& outer-stroke {:shape shape :index index}
|
||||||
child]
|
child]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
|
|
|
@ -109,7 +109,8 @@
|
||||||
|
|
||||||
(cond-> text?
|
(cond-> text?
|
||||||
(-> (add! :grow-type)
|
(-> (add! :grow-type)
|
||||||
(add! :content (comp json/encode uuid->string))))
|
(add! :content (comp json/encode uuid->string))
|
||||||
|
(add! :position-data (comp json/encode uuid->string))))
|
||||||
|
|
||||||
(cond-> mask?
|
(cond-> mask?
|
||||||
(obj/set! "penpot:masked-group" "true"))
|
(obj/set! "penpot:masked-group" "true"))
|
||||||
|
@ -138,6 +139,7 @@
|
||||||
(into {} (map prefix-entry) m)))
|
(into {} (map prefix-entry) m)))
|
||||||
|
|
||||||
(defn- export-grid-data [{:keys [grids]}]
|
(defn- export-grid-data [{:keys [grids]}]
|
||||||
|
(when (d/not-empty? grids)
|
||||||
(mf/html
|
(mf/html
|
||||||
[:> "penpot:grids" #js {}
|
[:> "penpot:grids" #js {}
|
||||||
(for [{:keys [type display params]} grids]
|
(for [{:keys [type display params]} grids]
|
||||||
|
@ -150,7 +152,7 @@
|
||||||
(obj/set! "penpot:opacity" (get-in params [:color :opacity]))
|
(obj/set! "penpot:opacity" (get-in params [:color :opacity]))
|
||||||
(obj/set! "penpot:type" (d/name type))
|
(obj/set! "penpot:type" (d/name type))
|
||||||
(cond-> (some? display)
|
(cond-> (some? display)
|
||||||
(obj/set! "penpot:display" (str display))))]))]))
|
(obj/set! "penpot:display" (str display))))]))])))
|
||||||
|
|
||||||
(mf/defc export-flows
|
(mf/defc export-flows
|
||||||
[{:keys [flows]}]
|
[{:keys [flows]}]
|
||||||
|
|
|
@ -20,18 +20,23 @@
|
||||||
[props]
|
[props]
|
||||||
|
|
||||||
(let [shape (obj/get props "shape")
|
(let [shape (obj/get props "shape")
|
||||||
render-id (obj/get props "render-id")
|
render-id (obj/get props "render-id")]
|
||||||
{:keys [x y width height]} (:selrect shape)
|
|
||||||
|
(when (or (some? (:fill-image shape))
|
||||||
|
(#{:image :text} (:type shape))
|
||||||
|
(> (count (:fills shape)) 1)
|
||||||
|
(some :fill-color-gradient (:fills shape)))
|
||||||
|
|
||||||
|
(let [{:keys [x y width height]} (:selrect shape)
|
||||||
{:keys [metadata]} shape
|
{:keys [metadata]} shape
|
||||||
fill-id (str "fill-" render-id)
|
|
||||||
has-image (or metadata (:fill-image shape))
|
has-image (or metadata (:fill-image shape))
|
||||||
uri (if metadata
|
uri (if metadata
|
||||||
(cfg/resolve-file-media metadata)
|
(cfg/resolve-file-media metadata)
|
||||||
(cfg/resolve-file-media (:fill-image shape)))
|
(cfg/resolve-file-media (:fill-image shape)))
|
||||||
embed (embed/use-data-uris [uri])
|
embed (embed/use-data-uris [uri])
|
||||||
transform (gsh/transform-matrix shape)
|
transform (gsh/transform-matrix shape)
|
||||||
pattern-attrs (cond-> #js {:id fill-id
|
pattern-attrs (cond-> #js {:patternUnits "userSpaceOnUse"
|
||||||
:patternUnits "userSpaceOnUse"
|
|
||||||
:x x
|
:x x
|
||||||
:y y
|
:y y
|
||||||
:height height
|
:height height
|
||||||
|
@ -40,25 +45,28 @@
|
||||||
(= :path (:type shape))
|
(= :path (:type shape))
|
||||||
(obj/set! "patternTransform" transform))]
|
(obj/set! "patternTransform" transform))]
|
||||||
|
|
||||||
|
(for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))]
|
||||||
[:*
|
[:*
|
||||||
(for [[index value] (-> (d/enumerate (:fills shape [])) reverse)]
|
(for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)]
|
||||||
(cond (some? (:fill-color-gradient value))
|
(when (some? (:fill-color-gradient value))
|
||||||
(case (:type (:fill-color-gradient value))
|
(let [props #js {:id (str "fill-color-gradient_" render-id "_" fill-index)
|
||||||
:linear [:> grad/linear-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index)
|
|
||||||
:gradient (:fill-color-gradient value)
|
:gradient (:fill-color-gradient value)
|
||||||
:shape shape}]
|
:shape shape}]
|
||||||
:radial [:> grad/radial-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index)
|
(case (d/name (:type (:fill-color-gradient value)))
|
||||||
:gradient (:fill-color-gradient value)
|
"linear" [:> grad/linear-gradient props]
|
||||||
:shape shape}])))
|
"radial" [:> grad/radial-gradient props]))))
|
||||||
|
|
||||||
[:> :pattern pattern-attrs
|
|
||||||
|
(let [fill-id (str "fill-" shape-index "-" render-id)]
|
||||||
|
[:> :pattern (-> (obj/clone pattern-attrs)
|
||||||
|
(obj/set! "id" fill-id))
|
||||||
[:g
|
[:g
|
||||||
(for [[index value] (-> (d/enumerate (:fills shape [])) reverse)]
|
(for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)]
|
||||||
[:> :rect (-> (attrs/extract-fill-attrs value index)
|
[:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index)
|
||||||
(obj/set! "width" width)
|
(obj/set! "width" width)
|
||||||
(obj/set! "height" height))])
|
(obj/set! "height" height))])
|
||||||
|
|
||||||
(when has-image
|
(when has-image
|
||||||
[:image {:xlinkHref (get embed uri uri)
|
[:image {:xlinkHref (get embed uri uri)
|
||||||
:width width
|
:width width
|
||||||
:height height}])]]]))
|
:height height}])]])])))))
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.gradients
|
(ns app.main.ui.shapes.gradients
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
@ -99,13 +100,14 @@
|
||||||
[props]
|
[props]
|
||||||
(let [attr (obj/get props "attr")
|
(let [attr (obj/get props "attr")
|
||||||
shape (obj/get props "shape")
|
shape (obj/get props "shape")
|
||||||
render-id (mf/use-ctx muc/render-ctx)
|
id (obj/get props "id")
|
||||||
id (str (name attr) "_" render-id)
|
id (or id (str (name attr) "_" (mf/use-ctx muc/render-ctx)))
|
||||||
gradient (get shape attr)
|
gradient (get shape attr)
|
||||||
gradient-props #js {:id id
|
gradient-props #js {:id id
|
||||||
:gradient gradient
|
:gradient gradient
|
||||||
:shape shape}]
|
:shape shape}]
|
||||||
(when gradient
|
(when gradient
|
||||||
(case (:type gradient)
|
(case (d/name (:type gradient))
|
||||||
:linear [:> linear-gradient gradient-props]
|
"linear" [:> linear-gradient gradient-props]
|
||||||
:radial [:> radial-gradient gradient-props]))))
|
"radial" [:> radial-gradient gradient-props]
|
||||||
|
nil))))
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.mask
|
(ns app.main.ui.shapes.mask
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.geom.shapes.text :as gst]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
@ -29,6 +31,17 @@
|
||||||
(defn filter-url [render-id mask]
|
(defn filter-url [render-id mask]
|
||||||
(str "url(#" (filter-id render-id mask) ")"))
|
(str "url(#" (filter-id render-id mask) ")"))
|
||||||
|
|
||||||
|
(defn set-white-fill
|
||||||
|
[shape]
|
||||||
|
(let [update-color
|
||||||
|
(fn [data]
|
||||||
|
(-> data
|
||||||
|
(dissoc :fill-color :fill-opacity :fill-color-gradient)
|
||||||
|
(assoc :fills [{:fill-color "#FFFFFF" :fill-opacity 1}])))]
|
||||||
|
(-> shape
|
||||||
|
(d/update-when :position-data #(mapv update-color %))
|
||||||
|
(assoc :stroke-color "#FFFFFF" :stroke-opacity 1))))
|
||||||
|
|
||||||
(defn mask-factory
|
(defn mask-factory
|
||||||
[shape-wrapper]
|
[shape-wrapper]
|
||||||
(mf/fnc mask-shape
|
(mf/fnc mask-shape
|
||||||
|
@ -36,7 +49,23 @@
|
||||||
[props]
|
[props]
|
||||||
(let [mask (unchecked-get props "mask")
|
(let [mask (unchecked-get props "mask")
|
||||||
render-id (mf/use-ctx muc/render-ctx)
|
render-id (mf/use-ctx muc/render-ctx)
|
||||||
mask' (gsh/transform-shape mask)]
|
svg-text? (and (= :text (:type mask)) (some? (:position-data mask)))
|
||||||
|
|
||||||
|
mask (cond-> mask svg-text? set-white-fill)
|
||||||
|
|
||||||
|
mask-bb
|
||||||
|
(cond
|
||||||
|
svg-text?
|
||||||
|
(gst/position-data-points mask)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(-> (gsh/transform-shape mask)
|
||||||
|
(:points)))]
|
||||||
|
[:*
|
||||||
|
[:g {:opacity 0}
|
||||||
|
[:g {:id (str "shape-" (mask-id render-id mask))}
|
||||||
|
[:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]]
|
||||||
|
|
||||||
[:defs
|
[:defs
|
||||||
[:filter {:id (filter-id render-id mask)}
|
[:filter {:id (filter-id render-id mask)}
|
||||||
[:feFlood {:flood-color "white"
|
[:feFlood {:flood-color "white"
|
||||||
|
@ -50,11 +79,14 @@
|
||||||
;; we cannot use clips instead of mask because clips can only be simple shapes
|
;; we cannot use clips instead of mask because clips can only be simple shapes
|
||||||
[:clipPath {:class "mask-clip-path"
|
[:clipPath {:class "mask-clip-path"
|
||||||
:id (clip-id render-id mask)}
|
:id (clip-id render-id mask)}
|
||||||
[:polyline {:points (->> (:points mask')
|
[:polyline {:points (->> mask-bb
|
||||||
(map #(str (:x %) "," (:y %)))
|
(map #(str (:x %) "," (:y %)))
|
||||||
(str/join " "))}]]
|
(str/join " "))}]]
|
||||||
|
|
||||||
[:mask {:class "mask-shape"
|
[:mask {:class "mask-shape"
|
||||||
:id (mask-id render-id mask)}
|
:id (mask-id render-id mask)}
|
||||||
[:g {:filter (filter-url render-id mask)}
|
;; SVG texts are broken in Firefox with the filter. When the masking shapes is a text
|
||||||
[:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]]])))
|
;; we use the `set-white-fill` instead of using the filter
|
||||||
|
[:g {:filter (when-not svg-text? (filter-url render-id mask))}
|
||||||
|
[:use {:href (str "#shape-" (mask-id render-id mask))}]]]]])))
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.shapes.attrs :as attrs]
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
[app.main.ui.shapes.custom-stroke :as cs]
|
|
||||||
[app.main.ui.shapes.export :as ed]
|
[app.main.ui.shapes.export :as ed]
|
||||||
[app.main.ui.shapes.fills :as fills]
|
[app.main.ui.shapes.fills :as fills]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
|
@ -64,11 +63,6 @@
|
||||||
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
|
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
|
||||||
(when (or (some? (:fill-image shape))
|
[:& fills/fills {:shape shape :render-id render-id}]
|
||||||
(= :image (:type shape))
|
|
||||||
(> (count (:fills shape)) 1)
|
|
||||||
(some :fill-color-gradient (:fills shape)))
|
|
||||||
[:& fills/fills {:shape shape :render-id render-id}])
|
|
||||||
[:& cs/stroke-defs {:shape shape :render-id render-id}]
|
|
||||||
[:& frame/frame-clip-def {:shape shape :render-id render-id}]]
|
[:& frame/frame-clip-def {:shape shape :render-id render-id}]]
|
||||||
children]]))
|
children]]))
|
||||||
|
|
|
@ -6,213 +6,16 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.text
|
(ns app.main.ui.shapes.text
|
||||||
(:require
|
(:require
|
||||||
[app.common.colors :as clr]
|
[app.main.ui.shapes.text.fo-text :as fo]
|
||||||
[app.common.data :as d]
|
[app.main.ui.shapes.text.svg-text :as svg]
|
||||||
[app.common.geom.shapes :as geom]
|
|
||||||
[app.main.ui.context :as muc]
|
|
||||||
[app.main.ui.shapes.attrs :as attrs]
|
|
||||||
[app.main.ui.shapes.text.styles :as sts]
|
|
||||||
[app.util.color :as uc]
|
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(mf/defc render-text
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [node (obj/get props "node")
|
|
||||||
text (:text node)
|
|
||||||
style (sts/generate-text-styles node)]
|
|
||||||
[:span.text-node {:style style}
|
|
||||||
(if (= text "") "\u00A0" text)]))
|
|
||||||
|
|
||||||
(mf/defc render-root
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [node (obj/get props "node")
|
|
||||||
children (obj/get props "children")
|
|
||||||
shape (obj/get props "shape")
|
|
||||||
style (sts/generate-root-styles shape node)]
|
|
||||||
[:div.root.rich-text
|
|
||||||
{:style style
|
|
||||||
:xmlns "http://www.w3.org/1999/xhtml"}
|
|
||||||
children]))
|
|
||||||
|
|
||||||
(mf/defc render-paragraph-set
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [children (obj/get props "children")
|
|
||||||
shape (obj/get props "shape")
|
|
||||||
style (sts/generate-paragraph-set-styles shape)]
|
|
||||||
[:div.paragraph-set {:style style} children]))
|
|
||||||
|
|
||||||
(mf/defc render-paragraph
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [node (obj/get props "node")
|
|
||||||
shape (obj/get props "shape")
|
|
||||||
children (obj/get props "children")
|
|
||||||
style (sts/generate-paragraph-styles shape node)
|
|
||||||
dir (:text-direction node "auto")]
|
|
||||||
[:p.paragraph {:style style :dir dir} children]))
|
|
||||||
|
|
||||||
;; -- Text nodes
|
|
||||||
(mf/defc render-node
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [{:keys [type text children] :as node} (obj/get props "node")]
|
|
||||||
(if (string? text)
|
|
||||||
[:> render-text props]
|
|
||||||
(let [component (case type
|
|
||||||
"root" render-root
|
|
||||||
"paragraph-set" render-paragraph-set
|
|
||||||
"paragraph" render-paragraph
|
|
||||||
nil)]
|
|
||||||
(when component
|
|
||||||
[:> component props
|
|
||||||
(for [[index node] (d/enumerate children)]
|
|
||||||
(let [props (-> (obj/clone props)
|
|
||||||
(obj/set! "node" node)
|
|
||||||
(obj/set! "index" index)
|
|
||||||
(obj/set! "key" index))]
|
|
||||||
[:> render-node props]))])))))
|
|
||||||
|
|
||||||
(defn- next-color
|
|
||||||
"Given a set of colors try to get a color not yet used"
|
|
||||||
[colors]
|
|
||||||
(assert (set? colors))
|
|
||||||
(loop [current-rgb [0 0 0]]
|
|
||||||
(let [current-hex (uc/rgb->hex current-rgb)]
|
|
||||||
(if (contains? colors current-hex)
|
|
||||||
(recur (uc/next-rgb current-rgb))
|
|
||||||
current-hex))))
|
|
||||||
|
|
||||||
(defn- remap-colors
|
|
||||||
"Returns a new content replacing the original colors by their mapped 'simple color'"
|
|
||||||
[content color-mapping]
|
|
||||||
|
|
||||||
(cond-> content
|
|
||||||
(and (:fill-opacity content) (< (:fill-opacity content) 1.0))
|
|
||||||
(-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)]))
|
|
||||||
(assoc :fill-opacity 1.0))
|
|
||||||
|
|
||||||
(some? (:fill-color-gradient content))
|
|
||||||
(-> (assoc :fill-color (get color-mapping (:fill-color-gradient content)))
|
|
||||||
(assoc :fill-opacity 1.0)
|
|
||||||
(dissoc :fill-color-gradient))
|
|
||||||
|
|
||||||
(contains? content :children)
|
|
||||||
(update :children #(mapv (fn [node] (remap-colors node color-mapping)) %))))
|
|
||||||
|
|
||||||
(defn- fill->color
|
|
||||||
"Given a content node returns the information about that node fill color"
|
|
||||||
[{:keys [fill-color fill-opacity fill-color-gradient]}]
|
|
||||||
|
|
||||||
(cond
|
|
||||||
(some? fill-color-gradient)
|
|
||||||
{:type :gradient
|
|
||||||
:gradient fill-color-gradient}
|
|
||||||
|
|
||||||
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
|
|
||||||
{:type :transparent
|
|
||||||
:hex fill-color
|
|
||||||
:opacity fill-opacity}
|
|
||||||
|
|
||||||
(string? fill-color)
|
|
||||||
{:type :solid
|
|
||||||
:hex fill-color
|
|
||||||
:map-to fill-color}))
|
|
||||||
|
|
||||||
(defn- retrieve-colors
|
|
||||||
"Given a text shape returns a triple with the values:
|
|
||||||
- colors used as fills
|
|
||||||
- a mapping from simple solid colors to complex ones (transparents/gradients)
|
|
||||||
- the inverse of the previous mapping (to restore the value in the SVG)"
|
|
||||||
[shape]
|
|
||||||
(let [color-data
|
|
||||||
(->> (:content shape)
|
|
||||||
(tree-seq map? :children)
|
|
||||||
(map fill->color)
|
|
||||||
(filter some?))
|
|
||||||
|
|
||||||
colors (->> color-data
|
|
||||||
(into #{clr/black}
|
|
||||||
(comp (filter #(= :solid (:type %)))
|
|
||||||
(map :hex))))
|
|
||||||
|
|
||||||
[colors color-data]
|
|
||||||
(loop [colors colors
|
|
||||||
head (first color-data)
|
|
||||||
tail (rest color-data)
|
|
||||||
result []]
|
|
||||||
|
|
||||||
(if (nil? head)
|
|
||||||
[colors result]
|
|
||||||
|
|
||||||
(if (= :solid (:type head))
|
|
||||||
(recur colors
|
|
||||||
(first tail)
|
|
||||||
(rest tail)
|
|
||||||
(conj result head))
|
|
||||||
|
|
||||||
(let [next-color (next-color colors)
|
|
||||||
head (assoc head :map-to next-color)
|
|
||||||
colors (conj colors next-color)]
|
|
||||||
(recur colors
|
|
||||||
(first tail)
|
|
||||||
(rest tail)
|
|
||||||
(conj result head))))))
|
|
||||||
|
|
||||||
color-mapping-inverse
|
|
||||||
(->> color-data
|
|
||||||
(remove #(= :solid (:type %)))
|
|
||||||
(group-by :map-to)
|
|
||||||
(d/mapm #(first %2)))
|
|
||||||
|
|
||||||
color-mapping
|
|
||||||
(merge
|
|
||||||
(->> color-data
|
|
||||||
(filter #(= :transparent (:type %)))
|
|
||||||
(map #(vector [(:hex %) (:opacity %)] (:map-to %)))
|
|
||||||
(into {}))
|
|
||||||
|
|
||||||
(->> color-data
|
|
||||||
(filter #(= :gradient (:type %)))
|
|
||||||
(map #(vector (:gradient %) (:map-to %)))
|
|
||||||
(into {})))]
|
|
||||||
|
|
||||||
[colors color-mapping color-mapping-inverse]))
|
|
||||||
|
|
||||||
(mf/defc text-shape
|
(mf/defc text-shape
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false}
|
||||||
::mf/forward-ref true}
|
[props]
|
||||||
[props ref]
|
|
||||||
(let [{:keys [id x y width height content] :as shape} (obj/get props "shape")
|
|
||||||
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
|
|
||||||
;; We add 8px to add a padding for the exporter
|
|
||||||
;; width (+ width 8)
|
|
||||||
[colors color-mapping color-mapping-inverse] (retrieve-colors shape)
|
|
||||||
|
|
||||||
plain-colors? (mf/use-ctx muc/text-plain-colors-ctx)
|
(let [{:keys [position-data]} (obj/get props "shape")]
|
||||||
|
(if (some? position-data)
|
||||||
content (cond-> content
|
[:> svg/text-shape props]
|
||||||
plain-colors?
|
[:> fo/text-shape props])))
|
||||||
(remap-colors color-mapping))]
|
|
||||||
|
|
||||||
[:foreignObject {:x x
|
|
||||||
:y y
|
|
||||||
:id id
|
|
||||||
:data-colors (->> colors (str/join ","))
|
|
||||||
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
|
||||||
:transform (geom/transform-matrix shape)
|
|
||||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
|
||||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
|
||||||
:style (-> (obj/new) (attrs/add-layer-props shape))
|
|
||||||
:ref ref}
|
|
||||||
;; We use a class here because react has a bug that won't use the appropriate selector for
|
|
||||||
;; `background-clip`
|
|
||||||
[:style ".text-node { background-clip: text;
|
|
||||||
-webkit-background-clip: text;" ]
|
|
||||||
[:& render-node {:index 0
|
|
||||||
:shape shape
|
|
||||||
:node content}]]))
|
|
||||||
|
|
225
frontend/src/app/main/ui/shapes/text/fo_text.cljs
Normal file
225
frontend/src/app/main/ui/shapes/text/fo_text.cljs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
;; 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.text.fo-text
|
||||||
|
(:require
|
||||||
|
[app.common.colors :as clr]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.shapes :as geom]
|
||||||
|
[app.main.ui.context :as muc]
|
||||||
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
|
[app.main.ui.shapes.text.styles :as sts]
|
||||||
|
[app.util.color :as uc]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(mf/defc render-text
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [node (obj/get props "node")
|
||||||
|
text (:text node)
|
||||||
|
style (sts/generate-text-styles node)]
|
||||||
|
[:span.text-node {:style style}
|
||||||
|
(if (= text "") "\u00A0" text)]))
|
||||||
|
|
||||||
|
(mf/defc render-root
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [node (obj/get props "node")
|
||||||
|
children (obj/get props "children")
|
||||||
|
shape (obj/get props "shape")
|
||||||
|
style (sts/generate-root-styles shape node)]
|
||||||
|
[:div.root.rich-text
|
||||||
|
{:style style
|
||||||
|
:xmlns "http://www.w3.org/1999/xhtml"}
|
||||||
|
children]))
|
||||||
|
|
||||||
|
(mf/defc render-paragraph-set
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [children (obj/get props "children")
|
||||||
|
shape (obj/get props "shape")
|
||||||
|
style (sts/generate-paragraph-set-styles shape)]
|
||||||
|
[:div.paragraph-set {:style style} children]))
|
||||||
|
|
||||||
|
(mf/defc render-paragraph
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [node (obj/get props "node")
|
||||||
|
shape (obj/get props "shape")
|
||||||
|
children (obj/get props "children")
|
||||||
|
style (sts/generate-paragraph-styles shape node)
|
||||||
|
dir (:text-direction node "auto")]
|
||||||
|
[:p.paragraph {:style style :dir dir} children]))
|
||||||
|
|
||||||
|
;; -- Text nodes
|
||||||
|
(mf/defc render-node
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [{:keys [type text children] :as node} (obj/get props "node")]
|
||||||
|
(if (string? text)
|
||||||
|
[:> render-text props]
|
||||||
|
(let [component (case type
|
||||||
|
"root" render-root
|
||||||
|
"paragraph-set" render-paragraph-set
|
||||||
|
"paragraph" render-paragraph
|
||||||
|
nil)]
|
||||||
|
(when component
|
||||||
|
[:> component props
|
||||||
|
(for [[index node] (d/enumerate children)]
|
||||||
|
(let [props (-> (obj/clone props)
|
||||||
|
(obj/set! "node" node)
|
||||||
|
(obj/set! "index" index)
|
||||||
|
(obj/set! "key" index))]
|
||||||
|
[:> render-node props]))])))))
|
||||||
|
|
||||||
|
(defn- next-color
|
||||||
|
"Given a set of colors try to get a color not yet used"
|
||||||
|
[colors]
|
||||||
|
(assert (set? colors))
|
||||||
|
(loop [current-rgb [0 0 0]]
|
||||||
|
(let [current-hex (uc/rgb->hex current-rgb)]
|
||||||
|
(if (contains? colors current-hex)
|
||||||
|
(recur (uc/next-rgb current-rgb))
|
||||||
|
current-hex))))
|
||||||
|
|
||||||
|
(defn- remap-colors
|
||||||
|
"Returns a new content replacing the original colors by their mapped 'simple color'"
|
||||||
|
[content color-mapping]
|
||||||
|
|
||||||
|
(cond-> content
|
||||||
|
(and (:fill-opacity content) (< (:fill-opacity content) 1.0))
|
||||||
|
(-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)]))
|
||||||
|
(assoc :fill-opacity 1.0))
|
||||||
|
|
||||||
|
(some? (:fill-color-gradient content))
|
||||||
|
(-> (assoc :fill-color (get color-mapping (:fill-color-gradient content)))
|
||||||
|
(assoc :fill-opacity 1.0)
|
||||||
|
(dissoc :fill-color-gradient))
|
||||||
|
|
||||||
|
(contains? content :children)
|
||||||
|
(update :children #(mapv (fn [node] (remap-colors node color-mapping)) %))))
|
||||||
|
|
||||||
|
(defn- fill->color
|
||||||
|
"Given a content node returns the information about that node fill color"
|
||||||
|
[{:keys [fill-color fill-opacity fill-color-gradient]}]
|
||||||
|
|
||||||
|
(cond
|
||||||
|
(some? fill-color-gradient)
|
||||||
|
{:type :gradient
|
||||||
|
:gradient fill-color-gradient}
|
||||||
|
|
||||||
|
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
|
||||||
|
{:type :transparent
|
||||||
|
:hex fill-color
|
||||||
|
:opacity fill-opacity}
|
||||||
|
|
||||||
|
(string? fill-color)
|
||||||
|
{:type :solid
|
||||||
|
:hex fill-color
|
||||||
|
:map-to fill-color}))
|
||||||
|
|
||||||
|
(defn- retrieve-colors
|
||||||
|
"Given a text shape returns a triple with the values:
|
||||||
|
- colors used as fills
|
||||||
|
- a mapping from simple solid colors to complex ones (transparents/gradients)
|
||||||
|
- the inverse of the previous mapping (to restore the value in the SVG)"
|
||||||
|
[shape]
|
||||||
|
(let [color-data
|
||||||
|
(->> (:content shape)
|
||||||
|
(tree-seq map? :children)
|
||||||
|
(map fill->color)
|
||||||
|
(filter some?))
|
||||||
|
|
||||||
|
colors (->> color-data
|
||||||
|
(into #{clr/black}
|
||||||
|
(comp (filter #(= :solid (:type %)))
|
||||||
|
(map :hex))))
|
||||||
|
|
||||||
|
[colors color-data]
|
||||||
|
(loop [colors colors
|
||||||
|
head (first color-data)
|
||||||
|
tail (rest color-data)
|
||||||
|
result []]
|
||||||
|
|
||||||
|
(if (nil? head)
|
||||||
|
[colors result]
|
||||||
|
|
||||||
|
(if (= :solid (:type head))
|
||||||
|
(recur colors
|
||||||
|
(first tail)
|
||||||
|
(rest tail)
|
||||||
|
(conj result head))
|
||||||
|
|
||||||
|
(let [next-color (next-color colors)
|
||||||
|
head (assoc head :map-to next-color)
|
||||||
|
colors (conj colors next-color)]
|
||||||
|
(recur colors
|
||||||
|
(first tail)
|
||||||
|
(rest tail)
|
||||||
|
(conj result head))))))
|
||||||
|
|
||||||
|
color-mapping-inverse
|
||||||
|
(->> color-data
|
||||||
|
(remove #(= :solid (:type %)))
|
||||||
|
(group-by :map-to)
|
||||||
|
(d/mapm #(first %2)))
|
||||||
|
|
||||||
|
color-mapping
|
||||||
|
(merge
|
||||||
|
(->> color-data
|
||||||
|
(filter #(= :transparent (:type %)))
|
||||||
|
(map #(vector [(:hex %) (:opacity %)] (:map-to %)))
|
||||||
|
(into {}))
|
||||||
|
|
||||||
|
(->> color-data
|
||||||
|
(filter #(= :gradient (:type %)))
|
||||||
|
(map #(vector (:gradient %) (:map-to %)))
|
||||||
|
(into {})))]
|
||||||
|
|
||||||
|
[colors color-mapping color-mapping-inverse]))
|
||||||
|
|
||||||
|
(mf/defc text-shape
|
||||||
|
{::mf/wrap-props false
|
||||||
|
::mf/forward-ref true}
|
||||||
|
[props ref]
|
||||||
|
(let [shape (obj/get props "shape")
|
||||||
|
transform (str (geom/transform-matrix shape))
|
||||||
|
|
||||||
|
{:keys [id x y width height content]} shape
|
||||||
|
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
|
||||||
|
;; We add 8px to add a padding for the exporter
|
||||||
|
;; width (+ width 8)
|
||||||
|
|
||||||
|
[colors color-mapping color-mapping-inverse] (retrieve-colors shape)
|
||||||
|
|
||||||
|
plain-colors? (mf/use-ctx muc/text-plain-colors-ctx)
|
||||||
|
|
||||||
|
content (cond-> content
|
||||||
|
plain-colors?
|
||||||
|
(remap-colors color-mapping))
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
[:foreignObject
|
||||||
|
{:x x
|
||||||
|
:y y
|
||||||
|
:id id
|
||||||
|
:data-colors (->> colors (str/join ","))
|
||||||
|
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
||||||
|
:transform transform
|
||||||
|
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||||
|
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
||||||
|
:style (-> (obj/new) (attrs/add-layer-props shape))
|
||||||
|
:ref ref}
|
||||||
|
;; We use a class here because react has a bug that won't use the appropriate selector for
|
||||||
|
;; `background-clip`
|
||||||
|
[:style ".text-node { background-clip: text;
|
||||||
|
-webkit-background-clip: text;" ]
|
||||||
|
[:& render-node {:index 0
|
||||||
|
:shape shape
|
||||||
|
:node content}]]))
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
|
[app.common.transit :as transit]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.util.color :as uc]
|
[app.util.color :as uc]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
@ -18,11 +19,12 @@
|
||||||
(let [valign (:vertical-align node "top")
|
(let [valign (:vertical-align node "top")
|
||||||
base #js {:height "100%"
|
base #js {:height "100%"
|
||||||
:width "100%"
|
:width "100%"
|
||||||
:fontFamily "sourcesanspro"}]
|
:fontFamily "sourcesanspro"
|
||||||
|
:display "flex"}]
|
||||||
(cond-> base
|
(cond-> base
|
||||||
(= valign "top") (obj/set! "justifyContent" "flex-start")
|
(= valign "top") (obj/set! "alignItems" "flex-start")
|
||||||
(= valign "center") (obj/set! "justifyContent" "center")
|
(= valign "center") (obj/set! "alignItems" "center")
|
||||||
(= valign "bottom") (obj/set! "justifyContent" "flex-end"))))
|
(= valign "bottom") (obj/set! "alignItems" "flex-end"))))
|
||||||
|
|
||||||
(defn generate-paragraph-set-styles
|
(defn generate-paragraph-set-styles
|
||||||
[{:keys [grow-type] :as shape}]
|
[{:keys [grow-type] :as shape}]
|
||||||
|
@ -33,12 +35,10 @@
|
||||||
;; the property it's known.
|
;; the property it's known.
|
||||||
;; `inline-flex` is similar to flex but `overflows` outside the bounds of the
|
;; `inline-flex` is similar to flex but `overflows` outside the bounds of the
|
||||||
;; parent
|
;; parent
|
||||||
(let [auto-width? (= grow-type :auto-width)
|
(let [auto-width? (= grow-type :auto-width)]
|
||||||
auto-height? (= grow-type :auto-height)]
|
|
||||||
#js {:display "inline-flex"
|
#js {:display "inline-flex"
|
||||||
:flexDirection "column"
|
:flexDirection "column"
|
||||||
:justifyContent "inherit"
|
:justifyContent "inherit"
|
||||||
:minHeight (when-not (or auto-width? auto-height?) "100%")
|
|
||||||
:minWidth (when-not auto-width? "100%")
|
:minWidth (when-not auto-width? "100%")
|
||||||
:marginRight "1px"
|
:marginRight "1px"
|
||||||
:verticalAlign "top"}))
|
:verticalAlign "top"}))
|
||||||
|
@ -58,7 +58,10 @@
|
||||||
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))
|
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))
|
||||||
|
|
||||||
(defn generate-text-styles
|
(defn generate-text-styles
|
||||||
[data]
|
([data]
|
||||||
|
(generate-text-styles data nil))
|
||||||
|
|
||||||
|
([data {:keys [show-text?] :or {show-text? true}}]
|
||||||
(let [letter-spacing (:letter-spacing data 0)
|
(let [letter-spacing (:letter-spacing data 0)
|
||||||
text-decoration (:text-decoration data)
|
text-decoration (:text-decoration data)
|
||||||
text-transform (:text-transform data)
|
text-transform (:text-transform data)
|
||||||
|
@ -71,29 +74,35 @@
|
||||||
fill-color (:fill-color data)
|
fill-color (:fill-color data)
|
||||||
fill-opacity (:fill-opacity data)
|
fill-opacity (:fill-opacity data)
|
||||||
|
|
||||||
;; Uncomment this to allow to remove text colors. This could break the texts that already exist
|
|
||||||
;;[r g b a] (if (nil? fill-color)
|
|
||||||
;; [0 0 0 0] ;; Transparent color
|
|
||||||
;; (uc/hex->rgba fill-color fill-opacity))
|
|
||||||
|
|
||||||
[r g b a] (uc/hex->rgba fill-color fill-opacity)
|
[r g b a] (uc/hex->rgba fill-color fill-opacity)
|
||||||
text-color (when (and (some? fill-color) (some? fill-opacity))
|
text-color (when (and (some? fill-color) (some? fill-opacity))
|
||||||
(str/format "rgba(%s, %s, %s, %s)" r g b a))
|
(str/format "rgba(%s, %s, %s, %s)" r g b a))
|
||||||
|
|
||||||
fontsdb (deref fonts/fontsdb)
|
fontsdb (deref fonts/fontsdb)
|
||||||
|
|
||||||
base #js {:textDecoration text-decoration
|
base #js {:textDecoration text-decoration
|
||||||
:textTransform text-transform
|
:textTransform text-transform
|
||||||
:lineHeight (or line-height "inherit")
|
:lineHeight (or line-height "inherit")
|
||||||
:color text-color}]
|
:color (if show-text? text-color "transparent")
|
||||||
|
:caretColor (or text-color "black")
|
||||||
|
:overflowWrap "initial"}
|
||||||
|
|
||||||
(when-let [gradient (:fill-color-gradient data)]
|
fills
|
||||||
(let [text-color (-> (update gradient :type keyword)
|
(cond
|
||||||
(uc/gradient->css))]
|
(some? (:fills data))
|
||||||
(-> base
|
(:fills data)
|
||||||
(obj/set! "--text-color" text-color)
|
|
||||||
(obj/set! "backgroundImage" "var(--text-color)")
|
(or (some? (:fill-color data))
|
||||||
(obj/set! "WebkitTextFillColor" "transparent")
|
(some? (:fill-opacity data))
|
||||||
(obj/set! "WebkitBackgroundClip" "text"))))
|
(some? (:fill-color-gradient data)))
|
||||||
|
[(d/without-nils (select-keys data [:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file]))]
|
||||||
|
|
||||||
|
(nil? (:fills data))
|
||||||
|
[{:fill-color "#000000" :fill-opacity 1}])
|
||||||
|
|
||||||
|
base (cond-> base
|
||||||
|
(some? fills)
|
||||||
|
(obj/set! "--fills" (transit/encode-str fills)))]
|
||||||
|
|
||||||
(when (and (string? letter-spacing)
|
(when (and (string? letter-spacing)
|
||||||
(pos? (alength letter-spacing)))
|
(pos? (alength letter-spacing)))
|
||||||
|
@ -120,4 +129,4 @@
|
||||||
(obj/set! base "fontStyle" font-style)
|
(obj/set! base "fontStyle" font-style)
|
||||||
(obj/set! base "fontWeight" font-weight)))
|
(obj/set! base "fontWeight" font-weight)))
|
||||||
|
|
||||||
base))
|
base)))
|
||||||
|
|
66
frontend/src/app/main/ui/shapes/text/svg_text.cljs
Normal file
66
frontend/src/app/main/ui/shapes/text/svg_text.cljs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
;; 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.text.svg-text
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.main.ui.context :as muc]
|
||||||
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
|
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
|
||||||
|
[app.main.ui.shapes.gradients :as grad]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(def fill-attrs [:fill-color :fill-color-gradient :fill-opacity])
|
||||||
|
|
||||||
|
(mf/defc text-shape
|
||||||
|
{::mf/wrap-props false
|
||||||
|
::mf/wrap [mf/memo]}
|
||||||
|
[props]
|
||||||
|
|
||||||
|
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||||
|
{:keys [x y width height position-data] :as shape} (obj/get props "shape")
|
||||||
|
transform (str (gsh/transform-matrix shape))
|
||||||
|
|
||||||
|
;; These position attributes are not really necesary but they are convenient for for the export
|
||||||
|
group-props (-> #js {:transform transform
|
||||||
|
:x x
|
||||||
|
:y y
|
||||||
|
:width width
|
||||||
|
:height height}
|
||||||
|
(attrs/add-style-attrs shape render-id))
|
||||||
|
get-gradient-id
|
||||||
|
(fn [index]
|
||||||
|
(str render-id "_" (:id shape) "_" index))]
|
||||||
|
[:*
|
||||||
|
;; Definition of gradients for partial elements
|
||||||
|
(when (d/seek :fill-color-gradient position-data)
|
||||||
|
[:defs
|
||||||
|
(for [[index data] (d/enumerate position-data)]
|
||||||
|
(when (some? (:fill-color-gradient data))
|
||||||
|
[:& grad/gradient {:id (str "fill-color-gradient_" (get-gradient-id index))
|
||||||
|
:attr :fill-color-gradient
|
||||||
|
:shape data}]))])
|
||||||
|
|
||||||
|
[:> :g group-props
|
||||||
|
(for [[index data] (d/enumerate position-data)]
|
||||||
|
(let [props (-> #js {:x (:x data)
|
||||||
|
:y (:y data)
|
||||||
|
:dominantBaseline "ideographic"
|
||||||
|
:style (-> #js {:fontFamily (:font-family data)
|
||||||
|
:fontSize (:font-size data)
|
||||||
|
:fontWeight (:font-weight data)
|
||||||
|
:textTransform (:text-transform data)
|
||||||
|
:textDecoration (:text-decoration data)
|
||||||
|
:fontStyle (:font-style data)
|
||||||
|
:direction (if (:rtl data) "rtl" "ltr")
|
||||||
|
:whiteSpace "pre"}
|
||||||
|
(obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))})]
|
||||||
|
[:& shape-custom-stroke {:shape shape :index index}
|
||||||
|
[:> :text props (:text data)]]))]]))
|
||||||
|
|
||||||
|
|
|
@ -108,8 +108,16 @@
|
||||||
(-> (cph/get-children objects (:id shape))
|
(-> (cph/get-children objects (:id shape))
|
||||||
(hooks/use-equal-memo))
|
(hooks/use-equal-memo))
|
||||||
|
|
||||||
|
all-svg-text?
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps all-children)
|
||||||
|
(fn []
|
||||||
|
(->> all-children
|
||||||
|
(filter #(= :text (:type %)))
|
||||||
|
(every? #(some? (:position-data %))))))
|
||||||
|
|
||||||
show-thumbnail?
|
show-thumbnail?
|
||||||
(and thumbnail? (some? (:thumbnail shape)))]
|
(and thumbnail? (some? (:thumbnail shape)) all-svg-text?)]
|
||||||
|
|
||||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||||
[:> shape-container {:shape shape}
|
[:> shape-container {:shape shape}
|
||||||
|
|
|
@ -6,16 +6,24 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.shapes.text
|
(ns app.main.ui.workspace.shapes.text
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.attrs :as attrs]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
[app.main.ui.shapes.text :as text]
|
[app.main.ui.shapes.text.fo-text :as fo]
|
||||||
|
[app.main.ui.shapes.text.svg-text :as svg]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
[app.util.svg :as usvg]
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
|
[app.util.text-svg-position :as utp]
|
||||||
[app.util.timers :as timers]
|
[app.util.timers :as timers]
|
||||||
[app.util.webapi :as wapi]
|
[app.util.webapi :as wapi]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
|
@ -29,18 +37,24 @@
|
||||||
|
|
||||||
(mf/defc text-static-content
|
(mf/defc text-static-content
|
||||||
[{:keys [shape]}]
|
[{:keys [shape]}]
|
||||||
[:& text/text-shape {:shape shape
|
[:& fo/text-shape {:shape shape
|
||||||
:grow-type (:grow-type shape)}])
|
:grow-type (:grow-type shape)}])
|
||||||
|
|
||||||
(defn- update-with-current-editor-state
|
(defn- update-with-current-editor-state
|
||||||
[{:keys [id] :as shape}]
|
[{:keys [id] :as shape}]
|
||||||
(let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state))
|
(let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state))
|
||||||
editor-state (mf/deref editor-state-ref)]
|
editor-state (mf/deref editor-state-ref)
|
||||||
(cond-> shape
|
|
||||||
(some? editor-state)
|
content (:content shape)
|
||||||
(assoc :content (-> editor-state
|
editor-content
|
||||||
|
(when editor-state
|
||||||
|
(-> editor-state
|
||||||
(ted/get-editor-current-content)
|
(ted/get-editor-current-content)
|
||||||
(ted/export-content))))))
|
(ted/export-content)))]
|
||||||
|
|
||||||
|
(cond-> shape
|
||||||
|
(some? editor-content)
|
||||||
|
(assoc :content (attrs/merge content editor-content)))))
|
||||||
|
|
||||||
(mf/defc text-resize-content
|
(mf/defc text-resize-content
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
|
@ -99,24 +113,115 @@
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(fn [] #(mf/set-ref-val! mnt false)))
|
(fn [] #(mf/set-ref-val! mnt false)))
|
||||||
|
|
||||||
[:& text/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}]))
|
[:& fo/text-shape {:ref text-ref-cb
|
||||||
|
:shape shape
|
||||||
|
:grow-type (:grow-type shape)
|
||||||
|
:key (str "shape-" (:id shape))}]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc text-wrapper
|
(mf/defc text-wrapper
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [{:keys [id] :as shape} (unchecked-get props "shape")
|
(let [{:keys [id position-data] :as shape} (unchecked-get props "shape")
|
||||||
edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local))
|
edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local))
|
||||||
edition? (mf/deref edition-ref)]
|
edition? (mf/deref edition-ref)
|
||||||
|
|
||||||
|
local-position-data (mf/use-state nil)
|
||||||
|
|
||||||
|
sid-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
handle-change-foreign-object
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [node]
|
||||||
|
(when-let [position-data (utp/calc-position-data node)]
|
||||||
|
(let [parent (dom/get-parent node)
|
||||||
|
parent-transform (dom/get-attribute parent "transform")
|
||||||
|
node-transform (dom/get-attribute node "transform")
|
||||||
|
|
||||||
|
parent-mtx (usvg/parse-transform parent-transform)
|
||||||
|
node-mtx (usvg/parse-transform node-transform)
|
||||||
|
|
||||||
|
;; We need to see what transformation is applied in the DOM to reverse it
|
||||||
|
;; before calculating the position data
|
||||||
|
mtx (-> (gmt/multiply parent-mtx node-mtx)
|
||||||
|
(gmt/inverse))
|
||||||
|
|
||||||
|
position-data
|
||||||
|
(->> position-data
|
||||||
|
(mapv #(merge % (-> (select-keys % [:x :y :width :height])
|
||||||
|
(gsh/transform-rect mtx)))))]
|
||||||
|
(reset! local-position-data position-data)))))
|
||||||
|
|
||||||
|
[node-ref on-change-node] (use-mutable-observer handle-change-foreign-object)
|
||||||
|
|
||||||
|
show-svg-text? (or (some? position-data) (some? @local-position-data))
|
||||||
|
|
||||||
|
shape
|
||||||
|
(cond-> shape
|
||||||
|
(some? @local-position-data)
|
||||||
|
(assoc :position-data @local-position-data))
|
||||||
|
|
||||||
|
update-position-data
|
||||||
|
(fn []
|
||||||
|
(when (some? @local-position-data)
|
||||||
|
(reset! local-position-data nil)
|
||||||
|
(st/emit! (dch/update-shapes
|
||||||
|
[id]
|
||||||
|
(fn [shape]
|
||||||
|
(-> shape
|
||||||
|
(assoc :position-data @local-position-data)))
|
||||||
|
{:save-undo? false}))))]
|
||||||
|
|
||||||
|
(mf/use-layout-effect
|
||||||
|
(mf/deps @local-position-data)
|
||||||
|
(fn []
|
||||||
|
;; Timer to update the shape. We do this so a lot of changes won't produce
|
||||||
|
;; a lot of updates (kind of a debounce)
|
||||||
|
(let [sid (timers/schedule 50 update-position-data)]
|
||||||
|
(fn []
|
||||||
|
(rx/dispose! sid)))))
|
||||||
|
|
||||||
|
(mf/use-layout-effect
|
||||||
|
(mf/deps show-svg-text?)
|
||||||
|
(fn []
|
||||||
|
(when-not show-svg-text?
|
||||||
|
;; There is no position data we need to calculate it even if no change has happened
|
||||||
|
;; this usualy happens the first time a text is rendered
|
||||||
|
(let [update-data
|
||||||
|
(fn update-data []
|
||||||
|
(let [node (mf/ref-val node-ref)]
|
||||||
|
(if (some? node)
|
||||||
|
(let [position-data (utp/calc-position-data node)]
|
||||||
|
(reset! local-position-data position-data))
|
||||||
|
|
||||||
|
;; No node present, we need to keep waiting
|
||||||
|
(do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid))
|
||||||
|
(when-not @local-position-data
|
||||||
|
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))]
|
||||||
|
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data))))
|
||||||
|
|
||||||
|
(fn []
|
||||||
|
(when-let [sid (mf/ref-val sid-ref)]
|
||||||
|
(rx/dispose! sid)))))
|
||||||
|
|
||||||
[:> shape-container {:shape shape}
|
[:> shape-container {:shape shape}
|
||||||
;; We keep hidden the shape when we're editing so it keeps track of the size
|
;; We keep hidden the shape when we're editing so it keeps track of the size
|
||||||
;; and updates the selrect accordingly
|
;; and updates the selrect accordingly
|
||||||
[:g.text-shape {:opacity (when edition? 0)
|
[:*
|
||||||
|
[:g.text-shape {:ref on-change-node
|
||||||
|
:opacity (when show-svg-text? 0)
|
||||||
:pointer-events "none"}
|
:pointer-events "none"}
|
||||||
|
|
||||||
;; The `:key` prop here is mandatory because the
|
;; The `:key` prop here is mandatory because the
|
||||||
;; text-resize-content breaks a hooks rule and we can't reuse
|
;; text-resize-content breaks a hooks rule and we can't reuse
|
||||||
;; the component if the edition flag changes.
|
;; the component if the edition flag changes.
|
||||||
[:& text-resize-content {:shape shape
|
[:& text-resize-content {:shape
|
||||||
|
(cond-> shape
|
||||||
|
show-svg-text?
|
||||||
|
(dissoc :transform :transform-inverse))
|
||||||
:edition? edition?
|
:edition? edition?
|
||||||
:key (str id edition?)}]]]))
|
:key (str id edition?)}]]
|
||||||
|
|
||||||
|
(when show-svg-text?
|
||||||
|
[:g.text-svg {:pointer-events "none"}
|
||||||
|
[:& svg/text-shape {:shape shape}]])]]))
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.workspace.shapes.text.editor
|
(ns app.main.ui.workspace.shapes.text.editor
|
||||||
(:require
|
(:require
|
||||||
["draft-js" :as draft]
|
["draft-js" :as draft]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
|
@ -61,9 +62,9 @@
|
||||||
(-> (.getData content)
|
(-> (.getData content)
|
||||||
(.toJS)
|
(.toJS)
|
||||||
(js->clj :keywordize-keys true)
|
(js->clj :keywordize-keys true)
|
||||||
(sts/generate-text-styles))
|
(sts/generate-text-styles {:show-text? false}))
|
||||||
(-> (txt/styles-to-attrs styles)
|
(-> (txt/styles-to-attrs styles)
|
||||||
(sts/generate-text-styles))))
|
(sts/generate-text-styles {:show-text? false}))))
|
||||||
|
|
||||||
(def default-decorator
|
(def default-decorator
|
||||||
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
||||||
|
@ -207,9 +208,13 @@
|
||||||
|
|
||||||
[:div.text-editor
|
[:div.text-editor
|
||||||
{:ref self-ref
|
{:ref self-ref
|
||||||
:style {:cursor cur/text
|
:style {:cursor (cur/text (:rotation shape))
|
||||||
:width (:width shape)
|
:width (:width shape)
|
||||||
:height (:height shape)}
|
:height (:height shape)
|
||||||
|
;; We hide the editor when is blurred because otherwise the selection won't let us see
|
||||||
|
;; the underlying text. Use opacity because display or visibility won't allow to recover
|
||||||
|
;; focus afterwards.
|
||||||
|
:opacity (when @blurred 0)}
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:class (dom/classnames
|
:class (dom/classnames
|
||||||
:align-top (= (:vertical-align content "top") "top")
|
:align-top (= (:vertical-align content "top") "top")
|
||||||
|
@ -227,23 +232,34 @@
|
||||||
:ref on-editor
|
:ref on-editor
|
||||||
:editor-state state}]]))
|
:editor-state state}]]))
|
||||||
|
|
||||||
(mf/defc text-shape-edit
|
(defn translate-point-from-viewport
|
||||||
{::mf/wrap [mf/memo]
|
"Translate a point in the viewport into client coordinates"
|
||||||
::mf/wrap-props false
|
[pt viewport zoom]
|
||||||
::mf/forward-ref true}
|
(let [vbox (.. ^js viewport -viewBox -baseVal)
|
||||||
[props _]
|
box (gpt/point (.-x vbox) (.-y vbox))
|
||||||
(let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape")
|
zoom (gpt/point zoom)]
|
||||||
clip-id (str "clip-" id)]
|
(-> (gpt/subtract pt box)
|
||||||
[:g.text-editor {:clip-path (str "url(#" clip-id ")")}
|
(gpt/multiply zoom))))
|
||||||
[:defs
|
|
||||||
;; This clippath will cut the huge foreign object we use to calculate the automatic resize
|
|
||||||
[:clipPath {:id clip-id}
|
|
||||||
[:rect {:x x :y y
|
|
||||||
:width (+ width 8) :height (+ height 8)
|
|
||||||
:transform (gsh/transform-matrix shape)}]]]
|
|
||||||
[:foreignObject {:transform (gsh/transform-matrix shape)
|
|
||||||
:x x :y y
|
|
||||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
|
||||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)}
|
|
||||||
|
|
||||||
[:& text-shape-edit-html {:shape shape :key (str id)}]]]))
|
(mf/defc text-editor-viewport
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [shape (obj/get props "shape")
|
||||||
|
viewport-ref (obj/get props "viewport-ref")
|
||||||
|
zoom (obj/get props "zoom")
|
||||||
|
|
||||||
|
position
|
||||||
|
(-> (gpt/point (-> shape :selrect :x)
|
||||||
|
(-> shape :selrect :y))
|
||||||
|
(translate-point-from-viewport (mf/ref-val viewport-ref) zoom))]
|
||||||
|
|
||||||
|
[:div {:style {:position "absolute"
|
||||||
|
:left (str (:x position) "px")
|
||||||
|
:top (str (:y position) "px")
|
||||||
|
:pointer-events "all"
|
||||||
|
:transform (str (gsh/transform-matrix shape nil (gpt/point 0 0)))
|
||||||
|
:transform-origin "center center"}}
|
||||||
|
|
||||||
|
[:div {:style {:transform (str "scale(" zoom ")")
|
||||||
|
:transform-origin "top left"}}
|
||||||
|
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]))
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.sidebar.options.menus.fill
|
(ns app.main.ui.workspace.sidebar.options.menus.fill
|
||||||
(:require
|
(:require
|
||||||
[app.common.attrs :as attrs]
|
|
||||||
[app.common.colors :as clr]
|
[app.common.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
|
@ -17,7 +16,6 @@
|
||||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def fill-attrs
|
(def fill-attrs
|
||||||
|
@ -51,40 +49,6 @@
|
||||||
;; Excluding nil values
|
;; Excluding nil values
|
||||||
values (d/without-nils values)
|
values (d/without-nils values)
|
||||||
|
|
||||||
only-shapes? (and (contains? values :fills)
|
|
||||||
;; texts have :fill-* attributes, the rest of the shapes have :fills
|
|
||||||
(= (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0))
|
|
||||||
|
|
||||||
shapes-and-texts? (and (contains? values :fills)
|
|
||||||
;; texts have :fill-* attributes, the rest of the shapes have :fills
|
|
||||||
(> (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0))
|
|
||||||
|
|
||||||
;; Texts still have :fill-* attributes and the rest of the shapes just :fills so we need some extra calculation when multiple selection happens to detect them
|
|
||||||
plain-values (if (vector? (:fills values))
|
|
||||||
(concat (:fills values) [(dissoc values :fills)])
|
|
||||||
values)
|
|
||||||
|
|
||||||
plain-values (attrs/get-attrs-multi plain-values [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])
|
|
||||||
|
|
||||||
plain-values (if (empty? plain-values)
|
|
||||||
values
|
|
||||||
plain-values)
|
|
||||||
|
|
||||||
;; We must control some rare situations like
|
|
||||||
;; - Selecting texts and shapes with different fills
|
|
||||||
;; - Selecting a text and a shape with empty fills
|
|
||||||
plain-values (if (and shapes-and-texts?
|
|
||||||
(or
|
|
||||||
(= (:fills values) :multiple)
|
|
||||||
(= 0 (count (:fills values)))))
|
|
||||||
{:fills :multiple
|
|
||||||
:fill-color :multiple
|
|
||||||
:fill-opacity :multiple
|
|
||||||
:fill-color-ref-id :multiple
|
|
||||||
:fill-color-ref-file :multiple
|
|
||||||
:fill-color-gradient :multiple}
|
|
||||||
plain-values)
|
|
||||||
|
|
||||||
hide-fill-on-export? (:hide-fill-on-export values false)
|
hide-fill-on-export? (:hide-fill-on-export values false)
|
||||||
|
|
||||||
checkbox-ref (mf/use-ref)
|
checkbox-ref (mf/use-ref)
|
||||||
|
@ -110,12 +74,6 @@
|
||||||
(fn [index]
|
(fn [index]
|
||||||
(st/emit! (dc/reorder-fills ids index new-index)))))
|
(st/emit! (dc/reorder-fills ids index new-index)))))
|
||||||
|
|
||||||
on-change-mixed-shapes
|
|
||||||
(mf/use-callback
|
|
||||||
(mf/deps ids)
|
|
||||||
(fn [color]
|
|
||||||
(st/emit! (dc/change-fill-and-clear ids color))))
|
|
||||||
|
|
||||||
on-remove
|
on-remove
|
||||||
(fn [index]
|
(fn [index]
|
||||||
(fn []
|
(fn []
|
||||||
|
@ -155,12 +113,11 @@
|
||||||
[:div.element-set
|
[:div.element-set
|
||||||
[:div.element-set-title
|
[:div.element-set-title
|
||||||
[:span label]
|
[:span label]
|
||||||
(when (and (not disable-remove?) (not (= :multiple (:fills values))) only-shapes?)
|
(when (and (not disable-remove?) (not (= :multiple (:fills values))))
|
||||||
[:div.add-page {:on-click on-add} i/close])]
|
[:div.add-page {:on-click on-add} i/close])]
|
||||||
|
|
||||||
[:div.element-set-content
|
[:div.element-set-content
|
||||||
|
|
||||||
(if only-shapes?
|
|
||||||
(cond
|
(cond
|
||||||
(= :multiple (:fills values))
|
(= :multiple (:fills values))
|
||||||
[:div.element-set-options-group
|
[:div.element-set-options-group
|
||||||
|
@ -184,15 +141,6 @@
|
||||||
:on-detach (on-detach index)
|
:on-detach (on-detach index)
|
||||||
:on-remove (on-remove index)}])])
|
:on-remove (on-remove index)}])])
|
||||||
|
|
||||||
[:& color-row {:color {:color (:fill-color plain-values)
|
|
||||||
:opacity (:fill-opacity plain-values)
|
|
||||||
:id (:fill-color-ref-id plain-values)
|
|
||||||
:file-id (:fill-color-ref-file plain-values)
|
|
||||||
:gradient (:fill-color-gradient plain-values)}
|
|
||||||
:title (tr "workspace.options.fill")
|
|
||||||
:on-change on-change-mixed-shapes
|
|
||||||
:on-detach (on-detach 0)}])
|
|
||||||
|
|
||||||
(when (or (= type :frame)
|
(when (or (= type :frame)
|
||||||
(and (= type :multiple) (some? hide-fill-on-export?)))
|
(and (= type :multiple) (some? hide-fill-on-export?)))
|
||||||
[:div.input-checkbox
|
[:div.input-checkbox
|
||||||
|
|
|
@ -347,5 +347,3 @@
|
||||||
[:div.row-flex
|
[:div.row-flex
|
||||||
[:> grow-options opts]
|
[:> grow-options opts]
|
||||||
[:div.align-icons]]]]))
|
[:div.align-icons]]]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
:fill :text
|
:fill :text
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :ignore
|
:stroke :shape
|
||||||
:text :text}
|
:text :text}
|
||||||
|
|
||||||
:image
|
:image
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.sidebar.options.shapes.text
|
(ns app.main.ui.workspace.sidebar.options.shapes.text
|
||||||
(:require
|
(:require
|
||||||
[app.common.colors :as clr]
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
|
@ -16,6 +15,7 @@
|
||||||
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu text-fill-attrs root-attrs paragraph-attrs text-attrs]]
|
[app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu text-fill-attrs root-attrs paragraph-attrs text-attrs]]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
@ -29,19 +29,18 @@
|
||||||
|
|
||||||
layer-values (select-keys shape layer-attrs)
|
layer-values (select-keys shape layer-attrs)
|
||||||
|
|
||||||
fill-values (dwt/current-text-values
|
fill-values (-> (dwt/current-text-values
|
||||||
{:editor-state editor-state
|
{:editor-state editor-state
|
||||||
:shape shape
|
:shape shape
|
||||||
:attrs text-fill-attrs})
|
:attrs (conj text-fill-attrs :fills)})
|
||||||
|
(d/update-in-when [:fill-color-gradient :type] keyword))
|
||||||
|
|
||||||
fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword)
|
fill-values (if (not (contains? fill-values :fills))
|
||||||
|
;; Old fill format
|
||||||
|
{:fills [fill-values]}
|
||||||
|
fill-values)
|
||||||
|
|
||||||
fill-values (cond-> fill-values
|
stroke-values (select-keys shape stroke-attrs)
|
||||||
(not (contains? fill-values :fill-color)) (assoc :fill-color clr/black)
|
|
||||||
(not (contains? fill-values :fill-opacity)) (assoc :fill-opacity 1)
|
|
||||||
;; Keep for backwards compatibility
|
|
||||||
(:fill fill-values) (assoc :fill-color (:fill fill-values))
|
|
||||||
(:opacity fill-values) (assoc :fill-opacity (:fill fill-values)))
|
|
||||||
|
|
||||||
text-values (d/merge
|
text-values (d/merge
|
||||||
(select-keys shape [:grow-type])
|
(select-keys shape [:grow-type])
|
||||||
|
@ -76,8 +75,11 @@
|
||||||
[:& fill-menu
|
[:& fill-menu
|
||||||
{:ids ids
|
{:ids ids
|
||||||
:type type
|
:type type
|
||||||
:values fill-values
|
:values fill-values}]
|
||||||
:disable-remove? true}]
|
|
||||||
|
[:& stroke-menu {:ids ids
|
||||||
|
:type type
|
||||||
|
:values stroke-values}]
|
||||||
|
|
||||||
[:& shadow-menu
|
[:& shadow-menu
|
||||||
{:ids ids
|
{:ids ids
|
||||||
|
|
|
@ -84,7 +84,6 @@
|
||||||
|
|
||||||
;; REFS
|
;; REFS
|
||||||
viewport-ref (mf/use-ref nil)
|
viewport-ref (mf/use-ref nil)
|
||||||
raw-position-ref (mf/use-ref nil) ;; Stores the raw position of the cursor
|
|
||||||
|
|
||||||
;; VARS
|
;; VARS
|
||||||
disable-paste (mf/use-var false)
|
disable-paste (mf/use-var false)
|
||||||
|
@ -110,6 +109,8 @@
|
||||||
;; Only when we have all the selected shapes in one frame
|
;; Only when we have all the selected shapes in one frame
|
||||||
selected-frame (when (= (count selected-frames) 1) (get base-objects (first selected-frames)))
|
selected-frame (when (= (count selected-frames) 1) (get base-objects (first selected-frames)))
|
||||||
|
|
||||||
|
editing-shape (when edition (get base-objects edition))
|
||||||
|
|
||||||
create-comment? (= :comments drawing-tool)
|
create-comment? (= :comments drawing-tool)
|
||||||
drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode])))
|
drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode])))
|
||||||
(and (some? drawing-obj) (= :path (:type drawing-obj))))
|
(and (some? drawing-obj) (= :path (:type drawing-obj))))
|
||||||
|
@ -128,7 +129,7 @@
|
||||||
on-pointer-down (actions/on-pointer-down)
|
on-pointer-down (actions/on-pointer-down)
|
||||||
on-pointer-enter (actions/on-pointer-enter in-viewport?)
|
on-pointer-enter (actions/on-pointer-enter in-viewport?)
|
||||||
on-pointer-leave (actions/on-pointer-leave in-viewport?)
|
on-pointer-leave (actions/on-pointer-leave in-viewport?)
|
||||||
on-pointer-move (actions/on-pointer-move viewport-ref raw-position-ref zoom move-stream)
|
on-pointer-move (actions/on-pointer-move viewport-ref zoom move-stream)
|
||||||
on-pointer-up (actions/on-pointer-up)
|
on-pointer-up (actions/on-pointer-up)
|
||||||
on-move-selected (actions/on-move-selected hover hover-ids selected space?)
|
on-move-selected (actions/on-move-selected hover hover-ids selected space?)
|
||||||
on-menu-selected (actions/on-menu-selected hover hover-ids selected)
|
on-menu-selected (actions/on-menu-selected hover hover-ids selected)
|
||||||
|
@ -159,13 +160,15 @@
|
||||||
show-artboard-names? (contains? layout :display-artboard-names)
|
show-artboard-names? (contains? layout :display-artboard-names)
|
||||||
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
|
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
|
||||||
|
|
||||||
|
show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
||||||
|
|
||||||
disabled-guides? (or drawing-tool transform)]
|
disabled-guides? (or drawing-tool transform)]
|
||||||
|
|
||||||
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
|
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
|
||||||
(hooks/setup-viewport-size viewport-ref)
|
(hooks/setup-viewport-size viewport-ref)
|
||||||
(hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?)
|
(hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?)
|
||||||
(hooks/setup-keyboard alt? ctrl? space?)
|
(hooks/setup-keyboard alt? ctrl? space?)
|
||||||
(hooks/setup-hover-shapes page-id move-stream raw-position-ref base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom)
|
(hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom)
|
||||||
(hooks/setup-viewport-modifiers modifiers base-objects)
|
(hooks/setup-viewport-modifiers modifiers base-objects)
|
||||||
(hooks/setup-shortcuts node-editing? drawing-path?)
|
(hooks/setup-shortcuts node-editing? drawing-path?)
|
||||||
(hooks/setup-active-frames base-objects vbox hover active-frames)
|
(hooks/setup-active-frames base-objects vbox hover active-frames)
|
||||||
|
@ -176,6 +179,10 @@
|
||||||
[:& wtr/frame-renderer {:objects base-objects
|
[:& wtr/frame-renderer {:objects base-objects
|
||||||
:background background}]
|
:background background}]
|
||||||
|
|
||||||
|
(when show-text-editor?
|
||||||
|
[:& editor/text-editor-viewport {:shape editing-shape
|
||||||
|
:viewport-ref viewport-ref
|
||||||
|
:zoom zoom}])
|
||||||
(when show-comments?
|
(when show-comments?
|
||||||
[:& comments/comments-layer {:vbox vbox
|
[:& comments/comments-layer {:vbox vbox
|
||||||
:vport vport
|
:vport vport
|
||||||
|
@ -206,7 +213,8 @@
|
||||||
:style {:background-color background
|
:style {:background-color background
|
||||||
:pointer-events "none"}}
|
:pointer-events "none"}}
|
||||||
|
|
||||||
[:& use/export-page {:options options}]
|
(when (debug? :show-export-metadata)
|
||||||
|
[:& use/export-page {:options options}])
|
||||||
|
|
||||||
[:& (mf/provider use/include-metadata-ctx) {:value (debug? :show-export-metadata)}
|
[:& (mf/provider use/include-metadata-ctx) {:value (debug? :show-export-metadata)}
|
||||||
[:& (mf/provider embed/context) {:value true}
|
[:& (mf/provider embed/context) {:value true}
|
||||||
|
@ -267,9 +275,6 @@
|
||||||
:hover-shape @hover
|
:hover-shape @hover
|
||||||
:zoom zoom}])
|
:zoom zoom}])
|
||||||
|
|
||||||
(when text-editing?
|
|
||||||
[:& editor/text-shape-edit {:shape (get base-objects edition)}])
|
|
||||||
|
|
||||||
[:& widgets/frame-titles
|
[:& widgets/frame-titles
|
||||||
{:objects objects-modified
|
{:objects objects-modified
|
||||||
:selected selected
|
:selected selected
|
||||||
|
|
|
@ -350,14 +350,13 @@
|
||||||
(kbd/shift? event)
|
(kbd/shift? event)
|
||||||
(kbd/alt? event))))))))
|
(kbd/alt? event))))))))
|
||||||
|
|
||||||
(defn on-pointer-move [viewport-ref raw-position-ref zoom move-stream]
|
(defn on-pointer-move [viewport-ref zoom move-stream]
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps zoom move-stream)
|
(mf/deps zoom move-stream)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [raw-pt (dom/get-client-position event)
|
(let [raw-pt (dom/get-client-position event)
|
||||||
viewport (mf/ref-val viewport-ref)
|
viewport (mf/ref-val viewport-ref)
|
||||||
pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
|
pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
|
||||||
(mf/set-ref-val! raw-position-ref raw-pt)
|
|
||||||
(rx/push! move-stream pt)))))
|
(rx/push! move-stream pt)))))
|
||||||
|
|
||||||
(defn on-mouse-wheel [viewport-ref zoom]
|
(defn on-mouse-wheel [viewport-ref zoom]
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.geom.shapes.rect :as gshr]
|
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
[app.main.data.shortcuts :as dsc]
|
[app.main.data.shortcuts :as dsc]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
|
@ -98,17 +97,7 @@
|
||||||
(some #(cph/is-parent? objects % group-id))
|
(some #(cph/is-parent? objects % group-id))
|
||||||
(not))))
|
(not))))
|
||||||
|
|
||||||
(defn check-text-collision?
|
(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom]
|
||||||
"Checks if he current position `pos` overlaps any of the text-nodes for the given `text-id`"
|
|
||||||
[objects pos text-id]
|
|
||||||
(and (= :text (get-in objects [text-id :type]))
|
|
||||||
(let [collisions
|
|
||||||
(->> (dom/query-all (str "#shape-" text-id " .text-node"))
|
|
||||||
(map dom/get-bounding-rect)
|
|
||||||
(map dom/bounding-rect->rect))]
|
|
||||||
(not (some #(gshr/contains-point? % pos) collisions)))))
|
|
||||||
|
|
||||||
(defn setup-hover-shapes [page-id move-stream raw-position-ref objects transform selected ctrl? hover hover-ids hover-disabled? zoom]
|
|
||||||
(let [;; We use ref so we don't recreate the stream on a change
|
(let [;; We use ref so we don't recreate the stream on a change
|
||||||
zoom-ref (mf/use-ref zoom)
|
zoom-ref (mf/use-ref zoom)
|
||||||
ctrl-ref (mf/use-ref @ctrl?)
|
ctrl-ref (mf/use-ref @ctrl?)
|
||||||
|
@ -180,9 +169,6 @@
|
||||||
|
|
||||||
remove-xfm (mapcat #(cph/get-parent-ids objects %))
|
remove-xfm (mapcat #(cph/get-parent-ids objects %))
|
||||||
remove-id? (cond-> (into #{} remove-xfm selected)
|
remove-id? (cond-> (into #{} remove-xfm selected)
|
||||||
:always
|
|
||||||
(into (filter #(check-text-collision? objects (mf/ref-val raw-position-ref) %)) ids)
|
|
||||||
|
|
||||||
(not @ctrl?)
|
(not @ctrl?)
|
||||||
(into (filter #(group-empty-space? % objects ids)) ids)
|
(into (filter #(group-empty-space? % objects ids)) ids)
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,9 @@
|
||||||
text?
|
text?
|
||||||
[shape-node
|
[shape-node
|
||||||
(dom/query shape-node "foreignObject")
|
(dom/query shape-node "foreignObject")
|
||||||
(dom/query shape-node ".text-shape")]
|
(dom/query shape-node ".text-shape")
|
||||||
|
(dom/query shape-node ".text-svg")
|
||||||
|
(dom/query shape-node ".text-clip")]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[shape-node])))
|
[shape-node])))
|
||||||
|
@ -118,18 +120,26 @@
|
||||||
|
|
||||||
[text-transform text-width text-height]
|
[text-transform text-width text-height]
|
||||||
(when (= :text type)
|
(when (= :text type)
|
||||||
(text-corrected-transform shape transform modifiers))]
|
(text-corrected-transform shape transform modifiers))
|
||||||
|
|
||||||
|
text-width (str text-width)
|
||||||
|
text-height (str text-height)]
|
||||||
|
|
||||||
(doseq [node nodes]
|
(doseq [node nodes]
|
||||||
(cond
|
(cond
|
||||||
(dom/class? node "text-shape")
|
(or (dom/class? node "text-shape") (dom/class? node "text-svg"))
|
||||||
(when (some? text-transform)
|
(when (some? text-transform)
|
||||||
(dom/set-attribute node "transform" (str text-transform)))
|
(dom/set-attribute node "transform" (str text-transform)))
|
||||||
|
|
||||||
(= (dom/get-tag-name node) "foreignObject")
|
(or (= (dom/get-tag-name node) "foreignObject")
|
||||||
(when (and (some? text-width) (some? text-height))
|
(dom/class? node "text-clip"))
|
||||||
(dom/set-attribute node "width" text-width)
|
(let [cur-width (dom/get-attribute node "width")
|
||||||
(dom/set-attribute node "height" text-height))
|
cur-height (dom/get-attribute node "height")]
|
||||||
|
(when (and (some? text-width) (not= cur-width text-width))
|
||||||
|
(dom/set-attribute node "width" text-width))
|
||||||
|
|
||||||
|
(when (and (some? text-height) (not= cur-height text-height))
|
||||||
|
(dom/set-attribute node "height" text-height)))
|
||||||
|
|
||||||
(and (some? transform) (some? node))
|
(and (some? transform) (some? node))
|
||||||
(dom/set-attribute node "transform" (str transform))))))))
|
(dom/set-attribute node "transform" (str transform))))))))
|
||||||
|
|
|
@ -264,11 +264,12 @@
|
||||||
:height (.-height ^js rect)}))
|
:height (.-height ^js rect)}))
|
||||||
|
|
||||||
(defn bounding-rect->rect
|
(defn bounding-rect->rect
|
||||||
[{:keys [left top width height]}]
|
[rect]
|
||||||
{:x left
|
(when (some? rect)
|
||||||
:y top
|
{:x (or (.-left rect) (:left rect))
|
||||||
:width width
|
:y (or (.-top rect) (:top rect))
|
||||||
:height height})
|
:width (or (.-width rect) (:width rect))
|
||||||
|
:height (or (.-height rect) (:height rect))}))
|
||||||
|
|
||||||
(defn get-window-size
|
(defn get-window-size
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -185,7 +185,7 @@
|
||||||
(d/deep-mapm
|
(d/deep-mapm
|
||||||
(fn [pair] (->> pair (mapv convert)))))))
|
(fn [pair] (->> pair (mapv convert)))))))
|
||||||
|
|
||||||
(def search-data-node? #{:rect :image :path :text :circle})
|
(def search-data-node? #{:rect :image :path :circle})
|
||||||
|
|
||||||
(defn get-svg-data
|
(defn get-svg-data
|
||||||
[type node]
|
[type node]
|
||||||
|
@ -200,6 +200,13 @@
|
||||||
(map #(:attrs %))
|
(map #(:attrs %))
|
||||||
(reduce add-attrs node-attrs)))
|
(reduce add-attrs node-attrs)))
|
||||||
|
|
||||||
|
(= type :text)
|
||||||
|
(->> node
|
||||||
|
(node-seq)
|
||||||
|
(filter #(contains? #{:g :foreignObject} (:tag %)))
|
||||||
|
(map #(:attrs %))
|
||||||
|
(reduce add-attrs node-attrs))
|
||||||
|
|
||||||
(= type :frame)
|
(= type :frame)
|
||||||
(let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))]
|
(let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))]
|
||||||
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
|
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
|
||||||
|
@ -482,7 +489,8 @@
|
||||||
[props node]
|
[props node]
|
||||||
(-> props
|
(-> props
|
||||||
(assoc :grow-type (get-meta node :grow-type keyword))
|
(assoc :grow-type (get-meta node :grow-type keyword))
|
||||||
(assoc :content (get-meta node :content (comp string->uuid json/decode)))))
|
(assoc :content (get-meta node :content (comp string->uuid json/decode)))
|
||||||
|
(assoc :position-data (get-meta node :position-data (comp string->uuid json/decode)))))
|
||||||
|
|
||||||
(defn add-group-data
|
(defn add-group-data
|
||||||
[props node]
|
[props node]
|
||||||
|
|
|
@ -95,6 +95,13 @@
|
||||||
selected (impl/getSelectedBlocks state)]
|
selected (impl/getSelectedBlocks state)]
|
||||||
(reduce update-blocks state selected)))
|
(reduce update-blocks state selected)))
|
||||||
|
|
||||||
|
(defn update-editor-current-inline-styles-fn
|
||||||
|
[state update-fn]
|
||||||
|
(let [attrs (-> (.getCurrentInlineStyle ^js state)
|
||||||
|
(txt/styles-to-attrs)
|
||||||
|
(update-fn))]
|
||||||
|
(impl/applyInlineStyle state (txt/attrs-to-styles attrs))))
|
||||||
|
|
||||||
(defn editor-split-block
|
(defn editor-split-block
|
||||||
[state]
|
[state]
|
||||||
(impl/splitBlockPreservingData state))
|
(impl/splitBlockPreservingData state))
|
||||||
|
|
124
frontend/src/app/util/text_svg_position.cljs
Normal file
124
frontend/src/app/util/text_svg_position.cljs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
;; 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.util.text-svg-position
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.transit :as transit]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.globals :as global]))
|
||||||
|
|
||||||
|
(defn get-range-rects
|
||||||
|
"Retrieve the rectangles that cover the selection given by a `node` adn
|
||||||
|
the start and end index `start-i`, `end-i`"
|
||||||
|
[^js node start-i end-i]
|
||||||
|
(let [^js range (.createRange global/document)]
|
||||||
|
(.setStart range node start-i)
|
||||||
|
(.setEnd range node end-i)
|
||||||
|
(.getClientRects range)))
|
||||||
|
|
||||||
|
;; TODO: Evaluate to change this function to Javascript
|
||||||
|
(defn parse-text-nodes
|
||||||
|
"Given a text node retrieves the rectangles for everyone of its paragraphs and its text."
|
||||||
|
[parent-node rtl text-node]
|
||||||
|
|
||||||
|
(let [content (.-textContent text-node)
|
||||||
|
text-size (.-length content)]
|
||||||
|
|
||||||
|
(loop [from-i 0
|
||||||
|
to-i 0
|
||||||
|
current ""
|
||||||
|
result []]
|
||||||
|
(if (>= to-i text-size)
|
||||||
|
(let [rects (get-range-rects text-node from-i to-i)
|
||||||
|
entry {:node parent-node
|
||||||
|
:position (dom/bounding-rect->rect (first rects))
|
||||||
|
:text current}]
|
||||||
|
;; We need to add the last element not closed yet
|
||||||
|
(conj result entry))
|
||||||
|
|
||||||
|
(let [rects (get-range-rects text-node from-i (inc to-i))]
|
||||||
|
;; If the rects increase means we're in a new paragraph
|
||||||
|
(if (> (.-length rects) 1)
|
||||||
|
(let [entry {:node parent-node
|
||||||
|
:position (dom/bounding-rect->rect (if rtl (second rects) (first rects)))
|
||||||
|
:text current}]
|
||||||
|
(recur to-i to-i "" (conj result entry)))
|
||||||
|
(recur from-i (inc to-i) (str current (nth content to-i)) result)))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn calc-text-node-positions
|
||||||
|
[base-node viewport zoom]
|
||||||
|
|
||||||
|
(when (and (some? base-node)(some? viewport))
|
||||||
|
(let [translate-point
|
||||||
|
(fn [pt]
|
||||||
|
(let [vbox (.. ^js viewport -viewBox -baseVal)
|
||||||
|
brect (dom/get-bounding-rect viewport)
|
||||||
|
brect (gpt/point (d/parse-integer (:left brect))
|
||||||
|
(d/parse-integer (:top brect)))
|
||||||
|
box (gpt/point (.-x vbox) (.-y vbox))
|
||||||
|
zoom (gpt/point zoom)]
|
||||||
|
|
||||||
|
(-> (gpt/subtract pt brect)
|
||||||
|
(gpt/divide zoom)
|
||||||
|
(gpt/add box))))
|
||||||
|
|
||||||
|
translate-rect
|
||||||
|
(fn [{:keys [x y width height] :as rect}]
|
||||||
|
(let [p1 (-> (gpt/point x y)
|
||||||
|
(translate-point))
|
||||||
|
|
||||||
|
p2 (-> (gpt/point (+ x width) (+ y height))
|
||||||
|
(translate-point))]
|
||||||
|
(assoc rect
|
||||||
|
:x (:x p1)
|
||||||
|
:y (:y p1)
|
||||||
|
:width (- (:x p2) (:x p1))
|
||||||
|
:height (- (:y p2) (:y p1)))))
|
||||||
|
|
||||||
|
text-nodes (dom/query-all base-node ".text-node, span[data-text]")]
|
||||||
|
|
||||||
|
(->> text-nodes
|
||||||
|
(mapcat
|
||||||
|
(fn [parent-node]
|
||||||
|
(let [rtl (= "rtl" (.-dir (.-parentElement parent-node)))]
|
||||||
|
(->> (.-childNodes parent-node)
|
||||||
|
(mapcat #(parse-text-nodes parent-node rtl %))))))
|
||||||
|
(mapv #(update % :position translate-rect))))))
|
||||||
|
|
||||||
|
(defn calc-position-data
|
||||||
|
[base-node]
|
||||||
|
(let [viewport (dom/get-element "render")
|
||||||
|
zoom (or (get-in @st/state [:workspace-local :zoom]) 1)]
|
||||||
|
(when (and (some? base-node) (some? viewport))
|
||||||
|
(let [text-data (calc-text-node-positions base-node viewport zoom)]
|
||||||
|
(when (d/not-empty? text-data)
|
||||||
|
(->> text-data
|
||||||
|
(mapv (fn [{:keys [node position text]}]
|
||||||
|
(let [{:keys [x y width height]} position
|
||||||
|
rtl (= "rtl" (.-dir (.-parentElement ^js node)))
|
||||||
|
styles (js/getComputedStyle ^js node)
|
||||||
|
get (fn [prop]
|
||||||
|
(let [value (.getPropertyValue styles prop)]
|
||||||
|
(when (and value (not= value ""))
|
||||||
|
value)))]
|
||||||
|
(d/without-nils
|
||||||
|
{:rtl rtl
|
||||||
|
:x (if rtl (+ x width) x)
|
||||||
|
:y (+ y height)
|
||||||
|
:width width
|
||||||
|
:height height
|
||||||
|
:font-family (str (get "font-family"))
|
||||||
|
:font-size (str (get "font-size"))
|
||||||
|
:font-weight (str (get "font-weight"))
|
||||||
|
:text-transform (str (get "text-transform"))
|
||||||
|
:text-decoration (str (get "text-decoration"))
|
||||||
|
:font-style (str (get "font-style"))
|
||||||
|
:fills (transit/decode-str (get "--fills"))
|
||||||
|
:text text}))))))))))
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.geom.shapes.text :as gte]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
@ -23,7 +24,15 @@
|
||||||
(defn index-shape
|
(defn index-shape
|
||||||
[objects parents-index clip-parents-index]
|
[objects parents-index clip-parents-index]
|
||||||
(fn [index shape]
|
(fn [index shape]
|
||||||
(let [{:keys [x y width height]} (gsh/points->selrect (:points shape))
|
(let [{:keys [x y width height]}
|
||||||
|
(cond
|
||||||
|
(and (= :text (:type shape))
|
||||||
|
(some? (:position-data shape))
|
||||||
|
(d/not-empty? (:position-data shape)))
|
||||||
|
(gte/position-data-bounding-box shape)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(gsh/points->selrect (:points shape)))
|
||||||
shape-bound #js {:x x :y y :width width :height height}
|
shape-bound #js {:x x :y y :width width :height height}
|
||||||
|
|
||||||
parents (get parents-index (:id shape))
|
parents (get parents-index (:id shape))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue