mirror of
https://github.com/penpot/penpot.git
synced 2025-07-12 16:57:17 +02:00
♻️ Change pixel overlay inner workings
This commit is contained in:
parent
537435372a
commit
9ce8c2d580
4 changed files with 88 additions and 141 deletions
|
@ -11,6 +11,7 @@
|
||||||
renderer iframes and interact with them using asyncrhonous
|
renderer iframes and interact with them using asyncrhonous
|
||||||
messages."
|
messages."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
@ -62,9 +63,11 @@
|
||||||
|
|
||||||
(defn render
|
(defn render
|
||||||
"Renders an SVG"
|
"Renders an SVG"
|
||||||
[{:keys [data styles width] :as params}]
|
[{:keys [data styles width result] :as params}]
|
||||||
(let [id (dm/str (uuid/next))
|
(let [styles (d/nilv styles "")
|
||||||
payload #js {:data data :styles styles :width width}
|
result (d/nilv result "blob")
|
||||||
|
id (dm/str (uuid/next))
|
||||||
|
payload #js {:data data :styles styles :width width :result result}
|
||||||
message #js {:id id
|
message #js {:id id
|
||||||
:scope "penpot/rasterizer"
|
:scope "penpot/rasterizer"
|
||||||
:payload payload}]
|
:payload payload}]
|
||||||
|
@ -83,11 +86,12 @@
|
||||||
|
|
||||||
(defn render-node
|
(defn render-node
|
||||||
"Renders an SVG using a node"
|
"Renders an SVG using a node"
|
||||||
[{:keys [node styles width] :as params}]
|
[{:keys [node styles width result] :as params}]
|
||||||
(let [width (or width (dom/get-attribute node "width"))
|
(let [width (d/nilv width (dom/get-attribute node "width"))
|
||||||
styles (or styles "")
|
styles (d/nilv styles "")
|
||||||
data (dom/node->xml node)]
|
data (dom/node->xml node)
|
||||||
(render {:data data :styles styles :width width})))
|
result (d/nilv result "blob")]
|
||||||
|
(render {:data data :styles styles :width width :result result})))
|
||||||
|
|
||||||
(defn init!
|
(defn init!
|
||||||
"Initializes the thumbnail renderer."
|
"Initializes the thumbnail renderer."
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
(if (some? style-node) (dom/node->xml style-node) "")
|
(if (some? style-node) (dom/node->xml style-node) "")
|
||||||
(dom/node->xml node))]
|
(dom/node->xml node))]
|
||||||
|
|
||||||
(->> (rx/of {:data svg-data :width fixed-width :styles ""})
|
(->> (rx/of {:data svg-data :width fixed-width})
|
||||||
(rx/mapcat thr/render)
|
(rx/mapcat thr/render)
|
||||||
(rx/map wapi/create-uri))))
|
(rx/map wapi/create-uri))))
|
||||||
|
|
||||||
|
|
|
@ -6,97 +6,34 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.viewport.pixel-overlay
|
(ns app.main.ui.workspace.viewport.pixel-overlay
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.pages.helpers :as cph]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.workspace.colors :as dwc]
|
[app.main.data.workspace.colors :as dwc]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
[app.main.rasterizer :as thr]
|
[app.main.rasterizer :as thr]
|
||||||
[app.main.refs :as refs]
|
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.css-cursors :as cur]
|
[app.main.ui.css-cursors :as cur]
|
||||||
[app.main.ui.workspace.shapes :as shapes]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.http :as http]
|
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.webapi :as wapi]
|
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[goog.events :as events]
|
[goog.events :as events]
|
||||||
[promesa.core :as p]
|
|
||||||
[rumext.v2 :as mf])
|
[rumext.v2 :as mf])
|
||||||
(:import goog.events.EventType))
|
(:import goog.events.EventType))
|
||||||
|
|
||||||
(defn- resolve-svg-images!
|
|
||||||
[svg-node]
|
|
||||||
(let [image-nodes (dom/query-all svg-node "image:not([href^=data])")
|
|
||||||
noop-fn (constantly nil)]
|
|
||||||
(->> (rx/from image-nodes)
|
|
||||||
(rx/mapcat
|
|
||||||
(fn [image]
|
|
||||||
(let [href (dom/get-attribute image "href")]
|
|
||||||
(->> (http/fetch {:method :get :uri href})
|
|
||||||
(rx/mapcat (fn [response] (.blob ^js response)))
|
|
||||||
(rx/mapcat wapi/read-file-as-data-url)
|
|
||||||
(rx/tap (fn [data]
|
|
||||||
(dom/set-attribute! image "href" data)))
|
|
||||||
(rx/reduce noop-fn))))))))
|
|
||||||
|
|
||||||
(defn- svg-as-data-url
|
|
||||||
"Transforms SVG as data-url resolving any blob, http or https url to
|
|
||||||
its data equivalent."
|
|
||||||
[svg]
|
|
||||||
(let [svg-clone (.cloneNode svg true)]
|
|
||||||
(->> (resolve-svg-images! svg-clone)
|
|
||||||
(rx/map (fn [_] (dom/svg-node->data-uri svg-clone))))))
|
|
||||||
|
|
||||||
(defn format-viewbox [vbox]
|
|
||||||
(str/join " " [(:x vbox 0)
|
|
||||||
(:y vbox 0)
|
|
||||||
(:width vbox 0)
|
|
||||||
(:height vbox 0)]))
|
|
||||||
|
|
||||||
(mf/defc overlay-frames
|
|
||||||
{::mf/wrap [mf/memo]
|
|
||||||
::mf/wrap-props false}
|
|
||||||
[]
|
|
||||||
(let [data (mf/deref refs/workspace-page)
|
|
||||||
objects (:objects data)
|
|
||||||
root (get objects uuid/zero)
|
|
||||||
shapes (->> (:shapes root)
|
|
||||||
(map (d/getf objects)))]
|
|
||||||
[:g.shapes
|
|
||||||
(for [shape shapes]
|
|
||||||
(cond
|
|
||||||
(not (cph/frame-shape? shape))
|
|
||||||
[:& shapes/shape-wrapper
|
|
||||||
{:shape shape
|
|
||||||
:key (:id shape)}]
|
|
||||||
|
|
||||||
(cph/is-direct-child-of-root? shape)
|
|
||||||
[:& shapes/root-frame-wrapper
|
|
||||||
{:shape shape
|
|
||||||
:key (:id shape)
|
|
||||||
:objects objects}]
|
|
||||||
|
|
||||||
:else
|
|
||||||
[:& shapes/nested-frame-wrapper
|
|
||||||
{:shape shape
|
|
||||||
:key (:id shape)
|
|
||||||
:objects objects}]))]))
|
|
||||||
|
|
||||||
(mf/defc pixel-overlay
|
(mf/defc pixel-overlay
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [vport (unchecked-get props "vport")
|
(let [vport (unchecked-get props "vport")
|
||||||
viewport-ref (unchecked-get props "viewport-ref")
|
|
||||||
viewport-node (mf/ref-val viewport-ref)
|
|
||||||
canvas-ref (mf/use-ref nil)
|
|
||||||
img-ref (mf/use-ref nil)
|
|
||||||
|
|
||||||
update-str (rx/subject)
|
viewport-ref (unchecked-get props "viewport-ref")
|
||||||
|
viewport-node (mf/ref-val viewport-ref)
|
||||||
|
|
||||||
|
canvas (js/OffscreenCanvas. (:width vport) (:height vport))
|
||||||
|
canvas-context (.getContext canvas "2d" #js {:willReadFrequently true})
|
||||||
|
canvas-image-data (mf/use-ref nil)
|
||||||
|
zoom-view-context (mf/use-ref nil)
|
||||||
|
|
||||||
|
update-str (rx/subject)
|
||||||
|
|
||||||
handle-keydown
|
handle-keydown
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -111,29 +48,39 @@
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps viewport-node)
|
(mf/deps viewport-node)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when-let [zoom-view-node (.getElementById js/document "picker-detail")]
|
(when-let [image-data (mf/ref-val canvas-image-data)]
|
||||||
(let [canvas-node (mf/ref-val canvas-ref)
|
(when-let [zoom-view-node (.getElementById js/document "picker-detail")]
|
||||||
|
(when-not (mf/ref-val zoom-view-context)
|
||||||
|
(mf/set-ref-val! zoom-view-context (.getContext zoom-view-node "2d")))
|
||||||
|
(let [{brx :left bry :top} (dom/get-bounding-rect viewport-node)
|
||||||
|
|
||||||
{brx :left bry :top} (dom/get-bounding-rect viewport-node)
|
x (- (.-clientX event) brx)
|
||||||
x (- (.-clientX event) brx)
|
y (- (.-clientY event) bry)
|
||||||
y (- (.-clientY event) bry)
|
|
||||||
|
|
||||||
zoom-context (.getContext zoom-view-node "2d" #js {:willReadFrequently true})
|
zoom-context (mf/ref-val zoom-view-context)
|
||||||
canvas-context (.getContext canvas-node "2d" #js {:willReadFrequently true})
|
|
||||||
pixel-data (.getImageData canvas-context x y 1 1)
|
offset (* (+ (* y (unchecked-get image-data "width")) x) 4)
|
||||||
rgba (.-data pixel-data)
|
rgba (unchecked-get image-data "data")
|
||||||
r (obj/get rgba 0)
|
|
||||||
g (obj/get rgba 1)
|
r (obj/get rgba (+ 0 offset))
|
||||||
b (obj/get rgba 2)
|
g (obj/get rgba (+ 1 offset))
|
||||||
a (obj/get rgba 3)
|
b (obj/get rgba (+ 2 offset))
|
||||||
area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)]
|
a (obj/get rgba (+ 3 offset))
|
||||||
(-> (js/createImageBitmap area-data)
|
|
||||||
(p/then
|
;; I don't know why, but the zoom view is offset by 24px
|
||||||
(fn [image]
|
;; instead of 25.
|
||||||
;; Draw area
|
sx (- x 24)
|
||||||
(obj/set! zoom-context "imageSmoothingEnabled" false)
|
sy (- y 20)
|
||||||
(.drawImage zoom-context image 0 0 200 160))))
|
sw 50
|
||||||
(st/emit! (dwc/pick-color [r g b a]))))))
|
sh 40
|
||||||
|
dx 0
|
||||||
|
dy 0
|
||||||
|
dw 200
|
||||||
|
dh 160]
|
||||||
|
(when (obj/get zoom-context "imageSmoothingEnabled")
|
||||||
|
(obj/set! zoom-context "imageSmoothingEnabled" false))
|
||||||
|
(.drawImage zoom-context canvas sx sy sw sh dx dy dw dh)
|
||||||
|
(st/emit! (dwc/pick-color [r g b a])))))))
|
||||||
|
|
||||||
handle-pointer-down-picker
|
handle-pointer-down-picker
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -152,33 +99,29 @@
|
||||||
(dwc/stop-picker))
|
(dwc/stop-picker))
|
||||||
(modal/disallow-click-outside!)))
|
(modal/disallow-click-outside!)))
|
||||||
|
|
||||||
handle-image-load
|
|
||||||
(mf/use-callback
|
|
||||||
(mf/deps img-ref)
|
|
||||||
(fn []
|
|
||||||
(let [canvas-node (mf/ref-val canvas-ref)
|
|
||||||
img-node (mf/ref-val img-ref)
|
|
||||||
canvas-context (.getContext canvas-node "2d")]
|
|
||||||
(.drawImage canvas-context img-node 0 0))))
|
|
||||||
|
|
||||||
handle-draw-picker-canvas
|
handle-draw-picker-canvas
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps img-ref)
|
|
||||||
(fn []
|
(fn []
|
||||||
(let [img-node (mf/ref-val img-ref)
|
(let [svg-node (dom/get-element "render")]
|
||||||
svg-node (dom/get-element "render")]
|
(->> (rx/of {:node svg-node
|
||||||
(->> (rx/of {:node svg-node})
|
:width (:width vport)
|
||||||
|
:result "image-bitmap"})
|
||||||
(rx/mapcat thr/render-node)
|
(rx/mapcat thr/render-node)
|
||||||
(rx/map wapi/create-uri)
|
(rx/subs (fn [image-bitmap]
|
||||||
(rx/tap #(js/console.log %))
|
(.drawImage canvas-context image-bitmap 0 0)
|
||||||
(rx/subs (fn [uri]
|
(let [width (unchecked-get canvas "width")
|
||||||
(obj/set! img-node "src" uri)))))))
|
height (unchecked-get canvas "height")
|
||||||
|
image-data (.getImageData canvas-context 0 0 width height)]
|
||||||
|
(mf/set-ref-val! canvas-image-data image-data))))))))
|
||||||
|
|
||||||
handle-svg-change
|
handle-svg-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn []
|
(fn []
|
||||||
(rx/push! update-str :update)))]
|
(rx/push! update-str :update)))]
|
||||||
|
|
||||||
|
(when (obj/get canvas-context "imageSmoothingEnabled")
|
||||||
|
(obj/set! canvas-context "imageSmoothingEnabled" false))
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(fn []
|
(fn []
|
||||||
(let [listener (events/listen js/document EventType.KEYDOWN handle-keydown)]
|
(let [listener (events/listen js/document EventType.KEYDOWN handle-keydown)]
|
||||||
|
@ -198,8 +141,7 @@
|
||||||
:subtree true
|
:subtree true
|
||||||
:characterData true}
|
:characterData true}
|
||||||
svg-node (dom/get-element "render")
|
svg-node (dom/get-element "render")
|
||||||
observer (js/MutationObserver. handle-svg-change)
|
observer (js/MutationObserver. handle-svg-change)]
|
||||||
]
|
|
||||||
(.observe observer svg-node config)
|
(.observe observer svg-node config)
|
||||||
(handle-svg-change)
|
(handle-svg-change)
|
||||||
|
|
||||||
|
@ -213,16 +155,4 @@
|
||||||
:class (cur/get-static "picker")
|
:class (cur/get-static "picker")
|
||||||
:on-pointer-down handle-pointer-down-picker
|
:on-pointer-down handle-pointer-down-picker
|
||||||
:on-pointer-up handle-pointer-up-picker
|
:on-pointer-up handle-pointer-up-picker
|
||||||
:on-pointer-move handle-pointer-move-picker}
|
:on-pointer-move handle-pointer-move-picker}]]))
|
||||||
[:div {:style {:display "none"}}
|
|
||||||
[:img {:ref img-ref
|
|
||||||
:on-load handle-image-load
|
|
||||||
:style {:position "absolute"
|
|
||||||
:width "100%"
|
|
||||||
:height "100%"}}]
|
|
||||||
[:canvas {:ref canvas-ref
|
|
||||||
:width (:width vport 0)
|
|
||||||
:height (:height vport 0)
|
|
||||||
:style {:position "absolute"
|
|
||||||
:width "100%"
|
|
||||||
:height "100%"}}]]]]))
|
|
||||||
|
|
|
@ -178,22 +178,35 @@
|
||||||
|
|
||||||
(constantly nil)))))
|
(constantly nil)))))
|
||||||
|
|
||||||
(defn- render
|
(defn- render-image-bitmap
|
||||||
"Renders a thumbnail using it's SVG and returns an ArrayBuffer of the image."
|
"Renders a thumbnail using it's SVG and returns an ImageBitmap of the image."
|
||||||
[payload]
|
[payload]
|
||||||
(let [data (unchecked-get payload "data")
|
(let [data (unchecked-get payload "data")
|
||||||
styles (unchecked-get payload "styles")
|
styles (unchecked-get payload "styles")
|
||||||
width (d/nilv (unchecked-get payload "width") 300)]
|
width (d/nilv (unchecked-get payload "width") 300)
|
||||||
|
quality (d/nilv (unchecked-get payload "quality") "medium")]
|
||||||
(->> (svg-prepare data styles width)
|
(->> (svg-prepare data styles width)
|
||||||
(rx/map #(wapi/create-blob % "image/svg+xml"))
|
(rx/map #(wapi/create-blob % "image/svg+xml"))
|
||||||
(rx/map wapi/create-uri)
|
(rx/map wapi/create-uri)
|
||||||
(rx/mapcat (fn [uri]
|
(rx/mapcat (fn [uri]
|
||||||
(->> (create-image uri)
|
(->> (create-image uri)
|
||||||
(rx/mapcat #(wapi/create-image-bitmap % #js {:resizeWidth width
|
(rx/mapcat #(wapi/create-image-bitmap % #js {:resizeWidth width
|
||||||
:resizeQuality "medium"}))
|
:resizeQuality quality}))
|
||||||
(rx/tap #(wapi/revoke-uri uri)))))
|
(rx/tap #(wapi/revoke-uri uri))))))))
|
||||||
|
|
||||||
(rx/mapcat bitmap->blob))))
|
(defn- render-blob
|
||||||
|
"Renders a thumbnail using it's SVG and returns a Blob of the image."
|
||||||
|
[payload]
|
||||||
|
(->> (render-image-bitmap payload)
|
||||||
|
(rx/mapcat bitmap->blob)))
|
||||||
|
|
||||||
|
(defn- render
|
||||||
|
"Renders a thumbnail and returns a stream."
|
||||||
|
[payload]
|
||||||
|
(let [result (d/nilv (unchecked-get payload "result") "blob")]
|
||||||
|
(case result
|
||||||
|
"image-bitmap" (render-image-bitmap payload)
|
||||||
|
(render-blob payload))))
|
||||||
|
|
||||||
(defn- on-message
|
(defn- on-message
|
||||||
"Handles messages from the main thread."
|
"Handles messages from the main thread."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue