mirror of
https://github.com/penpot/penpot.git
synced 2025-01-23 23:18:48 -05:00
✨ Initial SVG text support
This commit is contained in:
parent
c3f57cf900
commit
a411cbc640
12 changed files with 465 additions and 229 deletions
|
@ -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
|
||||||
|
(map #(-> %
|
||||||
|
(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)))))
|
||||||
|
|
||||||
|
@ -533,7 +542,8 @@
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (apply-displacement shape)
|
(let [shape (apply-displacement shape)
|
||||||
modifiers (:modifiers shape)]
|
modifiers (:modifiers shape)
|
||||||
|
shape (cond-> shape (= :text (:type shape)) (assoc :dirty? true))]
|
||||||
(cond-> shape
|
(cond-> shape
|
||||||
(not (empty-modifiers? modifiers))
|
(not (empty-modifiers? modifiers))
|
||||||
(-> (set-flip modifiers)
|
(-> (set-flip modifiers)
|
||||||
|
|
|
@ -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:")
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
(when (and (not= content (:content shape))
|
(when (and (not= content (:content shape))
|
||||||
(some? (:current-page-id state)))
|
(some? (:current-page-id state)))
|
||||||
(rx/of
|
(rx/of
|
||||||
(dch/update-shapes [id] #(assoc % :content content))
|
(dch/update-shapes [id] #(assoc % :content content :dirty? true))
|
||||||
(dwu/commit-undo-transaction)))))
|
(dwu/commit-undo-transaction)))))
|
||||||
|
|
||||||
(when (some? id)
|
(when (some? id)
|
||||||
|
@ -149,7 +149,9 @@
|
||||||
[shape pred-fn merge-fn attrs]
|
[shape pred-fn merge-fn attrs]
|
||||||
(let [merge-attrs #(merge-fn % attrs)
|
(let [merge-attrs #(merge-fn % attrs)
|
||||||
transform #(txt/transform-nodes pred-fn merge-attrs %)]
|
transform #(txt/transform-nodes pred-fn merge-attrs %)]
|
||||||
(update shape :content transform)))
|
(-> shape
|
||||||
|
(update :content transform)
|
||||||
|
(assoc :dirty? true))))
|
||||||
|
|
||||||
(defn update-root-attrs
|
(defn update-root-attrs
|
||||||
[{:keys [id attrs]}]
|
[{:keys [id attrs]}]
|
||||||
|
|
|
@ -185,6 +185,7 @@
|
||||||
:transform
|
:transform
|
||||||
:transform-inverse
|
:transform-inverse
|
||||||
:rotation
|
:rotation
|
||||||
|
:dirty?
|
||||||
:flip-x
|
:flip-x
|
||||||
:flip-y]})
|
:flip-y]})
|
||||||
(clear-local-transform)
|
(clear-local-transform)
|
||||||
|
|
|
@ -20,6 +20,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 +73,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)))
|
||||||
|
|
||||||
|
@ -92,7 +95,9 @@
|
||||||
text-shapes
|
text-shapes
|
||||||
(->> objects
|
(->> objects
|
||||||
(filter (fn [[_ shape]] (= :text (:type shape))))
|
(filter (fn [[_ shape]] (= :text (:type shape))))
|
||||||
(mapv second))]
|
(mapv second))
|
||||||
|
|
||||||
|
render-texts? (and render-texts? (some #(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}
|
||||||
|
|
|
@ -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}]]))
|
|
||||||
|
|
218
frontend/src/app/main/ui/shapes/text/fo_text.cljs
Normal file
218
frontend/src/app/main/ui/shapes/text/fo_text.cljs
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
;; 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 [{: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)
|
||||||
|
|
||||||
|
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 (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}]]))
|
45
frontend/src/app/main/ui/shapes/text/svg_text.cljs
Normal file
45
frontend/src/app/main/ui/shapes/text/svg_text.cljs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
;; 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.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(mf/defc text-shape
|
||||||
|
{::mf/wrap-props false
|
||||||
|
::mf/wrap [mf/memo]}
|
||||||
|
[props]
|
||||||
|
|
||||||
|
(let [{:keys [x y width height position-data] :as shape} (obj/get props "shape")
|
||||||
|
zoom (or (get-in @st/state [:workspace-local :zoom]) 1)]
|
||||||
|
[:text {:x x
|
||||||
|
:y y
|
||||||
|
:width width
|
||||||
|
:height height
|
||||||
|
:dominant-baseline "ideographic"
|
||||||
|
:transform (gsh/transform-matrix shape)
|
||||||
|
}
|
||||||
|
(for [data position-data]
|
||||||
|
[:tspan {:x (:x data)
|
||||||
|
:y (:y data)
|
||||||
|
:transform (:transform-inverse shape (gmt/matrix))
|
||||||
|
:style {:fill "black"
|
||||||
|
:fill-opacity 1
|
||||||
|
:stroke "red"
|
||||||
|
:stroke-width (/ 0.5 zoom)
|
||||||
|
:font-family (:font-family data)
|
||||||
|
:font-size (:font-size data)
|
||||||
|
:font-weight (:font-weight data)
|
||||||
|
:text-transform (:text-transform data)
|
||||||
|
:text-decoration (:text-decoration data)
|
||||||
|
:font-style (:font-style data)
|
||||||
|
:direction (if (:rtl? data) "rtl" "ltr")
|
||||||
|
:white-space "pre"}}
|
||||||
|
(: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}
|
||||||
|
|
|
@ -8,14 +8,17 @@
|
||||||
(:require
|
(:require
|
||||||
[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.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.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,7 +32,7 @@
|
||||||
|
|
||||||
(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
|
||||||
|
@ -99,24 +102,67 @@
|
||||||
(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)}]))
|
||||||
|
|
||||||
(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 dirty?] :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)
|
||||||
|
shape-ref (mf/use-ref nil)]
|
||||||
|
|
||||||
|
(mf/use-layout-effect
|
||||||
|
(mf/deps dirty?)
|
||||||
|
(fn []
|
||||||
|
(when (and (or dirty? (not (:position-data shape))) (some? id))
|
||||||
|
(let [base-node (mf/ref-val shape-ref)
|
||||||
|
viewport (dom/get-element "render")
|
||||||
|
zoom (get-in @st/state [:workspace-local :zoom])
|
||||||
|
text-data (utp/calc-text-node-positions base-node viewport zoom)
|
||||||
|
position-data
|
||||||
|
(->> text-data
|
||||||
|
(map (fn [{:keys [node position text]}]
|
||||||
|
(let [{:keys [x y width height]} position
|
||||||
|
rtl? (= "rtl" (.-dir (.-parentElement ^js node)))
|
||||||
|
styles (.computedStyleMap ^js node)]
|
||||||
|
{:rtl? rtl?
|
||||||
|
:x (if rtl? (+ x width) x)
|
||||||
|
:y (+ y height)
|
||||||
|
:width width
|
||||||
|
:height height
|
||||||
|
:font-family (str (.get styles "font-family"))
|
||||||
|
:font-size (str (.get styles "font-size"))
|
||||||
|
:font-weight (str (.get styles "font-weight"))
|
||||||
|
:text-transform (str (.get styles "text-transform"))
|
||||||
|
:text-decoration (str (.get styles "text-decoration"))
|
||||||
|
:font-style (str (.get styles "font-style"))
|
||||||
|
:text text}))))]
|
||||||
|
(st/emit! (dch/update-shapes
|
||||||
|
[id]
|
||||||
|
(fn [shape]
|
||||||
|
(-> shape
|
||||||
|
(dissoc :dirty?)
|
||||||
|
(assoc :position-data position-data)))))))))
|
||||||
|
|
||||||
[:> 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)
|
[:*
|
||||||
:pointer-events "none"}
|
[:g.text-shape {:ref shape-ref
|
||||||
|
:opacity (when (or edition? (some? (:position-data shape))) 0)
|
||||||
|
: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
|
||||||
:edition? edition?
|
(:position-data shape)
|
||||||
:key (str id edition?)}]]]))
|
(dissoc :transform :transform-inverse))
|
||||||
|
:edition? edition?
|
||||||
|
:key (str id edition?)}]]
|
||||||
|
|
||||||
|
[:g {:opacity (when edition? 0)
|
||||||
|
:pointer-events "none"}
|
||||||
|
(when (some? (:position-data shape))
|
||||||
|
[:& svg/text-shape {:shape shape}])]]]))
|
||||||
|
|
|
@ -264,11 +264,11 @@
|
||||||
:height (.-height ^js rect)}))
|
:height (.-height ^js rect)}))
|
||||||
|
|
||||||
(defn bounding-rect->rect
|
(defn bounding-rect->rect
|
||||||
[{:keys [left top width height]}]
|
[rect]
|
||||||
{:x left
|
{:x (or (.-left rect) (:left rect))
|
||||||
:y top
|
:y (or (.-top rect) (:top rect))
|
||||||
:width width
|
:width (or (.-width rect) (:width rect))
|
||||||
:height height})
|
:height (or (.-height rect) (:height rect))})
|
||||||
|
|
||||||
(defn get-window-size
|
(defn get-window-size
|
||||||
[]
|
[]
|
||||||
|
|
92
frontend/src/app/util/text_svg_position.cljs
Normal file
92
frontend/src/app/util/text_svg_position.cljs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
;; 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.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)))
|
||||||
|
|
||||||
|
(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 (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")]
|
||||||
|
|
||||||
|
(->> text-nodes
|
||||||
|
(mapcat
|
||||||
|
(fn [parent-node]
|
||||||
|
(let [rtl? (= "rtl" (.-dir (.-parentElement parent-node)))]
|
||||||
|
(->> (.-childNodes parent-node)
|
||||||
|
(mapcat #(parse-text-nodes parent-node rtl? %))))))
|
||||||
|
(map #(update % :position translate-rect))))))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue