diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index 88ba2ed5d..ed9d8bf0f 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -190,3 +190,20 @@ (multiply mtx) (translate (gpt/negate pt))) 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'))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index fe7efceef..2af461501 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -84,16 +84,18 @@ base #js {:textDecoration text-decoration :textTransform text-transform :lineHeight (or line-height "inherit") - :color text-color}] + :color text-color + :caretColor "black"}] (when-let [gradient (:fill-color-gradient data)] (let [text-color (-> (update gradient :type keyword) (uc/gradient->css))] (-> base - (obj/set! "--text-color" text-color) - (obj/set! "backgroundImage" "var(--text-color)") - (obj/set! "WebkitTextFillColor" "transparent") - (obj/set! "WebkitBackgroundClip" "text")))) + (obj/set! "color" text-color) + #_(obj/set! "--text-color" text-color) + #_(obj/set! "backgroundImage" "var(--text-color)") + #_(obj/set! "WebkitTextFillColor" "transparent") + #_(obj/set! "WebkitBackgroundClip" "text")))) (when (and (string? letter-spacing) (pos? (alength letter-spacing))) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index 3f6ff5313..607860b1a 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -23,9 +23,11 @@ [props] (let [render-id (mf/use-ctx muc/render-ctx) - {:keys [position-data] :as shape} (obj/get props "shape") - group-props (-> #js {:transform (gsh/transform-matrix shape)} - (attrs/add-style-attrs shape render-id)) + {:keys [id x y width height position-data] :as shape} (obj/get props "shape") + clip-id (str "clip-text" id "_" render-id) + group-props (-> #js {:transform (gsh/transform-matrix shape) + :clipPath (str "url(#" clip-id ")")} + (attrs/add-style-attrs shape render-id)) get-gradient-id (fn [index] (str render-id "_" (:id shape) "_" index))] @@ -41,10 +43,16 @@ [:& shape-custom-stroke {:shape shape} [:> :g group-props + [:defs + [:clipPath {:id clip-id} + [:rect.text-clip + {:x x :y y + :width width :height height + :transform (gsh/transform-matrix shape)}]]] (for [[index data] (d/enumerate position-data)] (let [props (-> #js {:x (:x data) :y (:y data) - :dominant-baseline "ideographic" + :dominantBaseline "ideographic" :style (-> #js {:fontFamily (:font-family data) :fontSize (:font-size data) :fontWeight (:font-weight data) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 741073d0d..cefb85a50 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,7 +6,10 @@ (ns app.main.ui.workspace.shapes.text (:require + [app.common.attrs :as attrs] [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.math :as mth] [app.common.transit :as transit] @@ -19,6 +22,7 @@ [app.main.ui.shapes.text.svg-text :as svg] [app.util.dom :as dom] [app.util.object :as obj] + [app.util.svg :as usvg] [app.util.text-editor :as ted] [app.util.text-svg-position :as utp] [app.util.timers :as timers] @@ -40,12 +44,18 @@ (defn- update-with-current-editor-state [{:keys [id] :as shape}] (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) + + content (:content shape) + editor-content + (when editor-state + (-> editor-state + (ted/get-editor-current-content) + (ted/export-content)))] + (cond-> shape - (some? editor-state) - (assoc :content (-> editor-state - (ted/get-editor-current-content) - (ted/export-content)))))) + (some? editor-content) + (assoc :content (attrs/merge content editor-content))))) (mf/defc text-resize-content {::mf/wrap-props false} @@ -106,69 +116,126 @@ [:& fo/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}])) + +(defn calc-position-data + [base-node] + (let [viewport (dom/get-element "render") + zoom (get-in @st/state [:workspace-local :zoom]) + text-data (utp/calc-text-node-positions base-node viewport zoom)] + (->> 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)] + (d/without-nils + {: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")) + :fill-color (or (dom/get-attribute node "data-fill-color") "#000000") + :fill-color-gradient (transit/decode-str (dom/get-attribute node "data-fill-color-gradient")) + :fill-opacity (d/parse-double (or (:fill-opacity node) "1")) + :text text}))))))) + + + (mf/defc text-wrapper {::mf/wrap-props false} [props] (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? (mf/deref edition-ref) - shape-ref (mf/use-ref nil)] + shape-ref (mf/use-ref nil) + + prev-obs-ref (mf/use-ref nil) + local-position-data (mf/use-state nil) + + handle-change-foreign-object + (fn [] + (when-let [node (mf/ref-val shape-ref)] + (let [position-data (calc-position-data node) + 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')))) + + on-change-node + (fn [^js node] + (mf/set-ref-val! shape-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 [fo-node (dom/query node "foreignObject") + options #js {:attributes true + :childList true + :subtree true} + mutation-obs (js/MutationObserver. handle-change-foreign-object)] + (mf/set-ref-val! prev-obs-ref mutation-obs) + (.observe mutation-obs fo-node options))))] + (mf/use-effect + (fn [] + (fn [] + (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] + (.disconnect prev-obs) + (mf/set-ref-val! prev-obs-ref nil))))) (mf/use-layout-effect - (mf/deps dirty?) + (mf/deps id 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)] - (d/without-nils - {: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")) - :fill-color (or (dom/get-attribute node "data-fill-color") "#000000") - :fill-color-gradient (transit/decode-str (dom/get-attribute node "data-fill-color-gradient")) - :fill-opacity (d/parse-double (or (:fill-opacity node) "1")) - :text text})))))] - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (dissoc :dirty?) - (assoc :position-data position-data))))))))) + (let [node (mf/ref-val shape-ref) + position-data (calc-position-data node)] + (reset! local-position-data nil) + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (dissoc :dirty?) + (assoc :position-data position-data)))))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly [:* - [:g.text-shape {:ref shape-ref + [:g.text-shape {:ref on-change-node :opacity (when (or edition? (some? (:position-data shape))) 0) :pointer-events "none"} ;; The `:key` prop here is mandatory because the ;; text-resize-content breaks a hooks rule and we can't reuse ;; the component if the edition flag changes. - [:& text-resize-content {:shape (cond-> shape - (:position-data shape) - (dissoc :transform :transform-inverse)) + [:& text-resize-content {:shape + (cond-> shape + (:position-data shape) + (dissoc :transform :transform-inverse)) :edition? edition? :key (str id edition?)}]] [:g.text-svg {:opacity (when edition? 0) :pointer-events "none"} (when (some? (:position-data shape)) - [:& svg/text-shape {:shape shape}])]]])) + [:& svg/text-shape {:shape (cond-> shape + (some? @local-position-data) + (assoc :position-data @local-position-data))}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 873061070..6e7db9411 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -105,7 +105,9 @@ text? [shape-node (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 [shape-node]))) @@ -118,18 +120,26 @@ [text-transform text-width text-height] (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] (cond - (dom/class? node "text-shape") + (or (dom/class? node "text-shape") (dom/class? node "text-svg")) (when (some? text-transform) (dom/set-attribute node "transform" (str text-transform))) - (= (dom/get-tag-name node) "foreignObject") - (when (and (some? text-width) (some? text-height)) - (dom/set-attribute node "width" text-width) - (dom/set-attribute node "height" text-height)) + (or (= (dom/get-tag-name node) "foreignObject") + (dom/class? node "text-clip")) + (let [cur-width (dom/get-attribute node "width") + 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)) (dom/set-attribute node "transform" (str transform))))))))