diff --git a/frontend/package.json b/frontend/package.json index 2b9c7213a9..05c6807e4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -79,6 +79,7 @@ "date-fns": "^2.30.0", "draft-js": "^0.11.7", "eventsource-parser": "^1.1.1", + "gl-matrix": "^3.4.3", "highlight.js": "^11.8.0", "js-beautify": "^1.14.9", "jszip": "^3.10.1", diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index acbfcce458..5dad3e552a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -261,7 +261,13 @@ (:y (first selected-shapes)) (:y selected-frame)) - rule-area-size (/ rules/rule-area-size zoom)] + rule-area-size (/ rules/rule-area-size zoom) + + ;; Aquí podemos configurar como queremos que sea el renderizado: + ;; - "gl" Utilizando sólo WebGL2 + ;; - "svg" Utilizando sólo SVG + ;; - "both" Utilizando ambos + renderer "both"] (hooks/setup-dom-events zoom disable-paste in-viewport? workspace-read-only?) (hooks/setup-viewport-size vport viewport-ref) @@ -305,54 +311,56 @@ [:& top-bar/top-bar]] - [:svg.render-shapes - {:id "render" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot "https://penpot.app/xmlns" - :preserveAspectRatio "xMidYMid meet" - :key (str "render" page-id) - :width (:width vport 0) - :height (:height vport 0) - :view-box (utils/format-viewbox vbox) - :style {:background-color background - :pointer-events "none"} - :fill "none"} + (when (or (= renderer "svg") (= renderer "both")) + [:svg.render-shapes + {:id "render" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot "https://penpot.app/xmlns" + :preserveAspectRatio "xMidYMid meet" + :key (str "render" page-id) + :width (:width vport 0) + :height (:height vport 0) + :view-box (utils/format-viewbox vbox) + :style {:background-color background + :pointer-events "none"} + :fill "none"} - [:defs - [:linearGradient {:id "frame-placeholder-gradient"} - [:animateTransform - {:attributeName "gradientTransform" - :type "translate" - :from "-1 0" - :to "1 0" - :dur "2s" - :repeatCount "indefinite"}] - [:stop {:offset "0%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}] - [:stop {:offset "50%" :stop-color (str "color-mix(in srgb-linear, " background " 80%, #777)") :stop-opacity 1}] - [:stop {:offset "100%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}]]] + [:defs + [:linearGradient {:id "frame-placeholder-gradient"} + [:animateTransform + {:attributeName "gradientTransform" + :type "translate" + :from "-1 0" + :to "1 0" + :dur "2s" + :repeatCount "indefinite"}] + [:stop {:offset "0%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}] + [:stop {:offset "50%" :stop-color (str "color-mix(in srgb-linear, " background " 80%, #777)") :stop-opacity 1}] + [:stop {:offset "100%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}]]] - (when (dbg/enabled? :show-export-metadata) - [:& use/export-page {:options options}]) + (when (dbg/enabled? :show-export-metadata) + [:& use/export-page {:options options}]) - ;; We need a "real" background shape so layer transforms work properly in firefox - [:rect {:width (:width vbox 0) - :height (:height vbox 0) - :x (:x vbox 0) - :y (:y vbox 0) - :fill background}] + ;; We need a "real" background shape so layer transforms work properly in firefox + [:rect {:width (:width vbox 0) + :height (:height vbox 0) + :x (:x vbox 0) + :y (:y vbox 0) + :fill background}] - [:& (mf/provider ctx/current-vbox) {:value vbox'} - [:& (mf/provider use/include-metadata-ctx) {:value (dbg/enabled? :show-export-metadata)} - ;; Render root shape - [:& shapes/root-shape {:key page-id - :objects base-objects - :active-frames @active-frames}]]]] + [:& (mf/provider ctx/current-vbox) {:value vbox'} + [:& (mf/provider use/include-metadata-ctx) {:value (dbg/enabled? :show-export-metadata)} + ;; Render root shape + [:& shapes/root-shape {:key page-id + :objects base-objects + :active-frames @active-frames}]]]]) ;; IT's MAGIC! - [gl/canvas {:objects base-objects - :active-frames @active-frames - :vbox vbox}] + (when (or (= renderer "gl") (= renderer "both")) + [gl/canvas {:objects base-objects + :active-frames @active-frames + :vbox vbox}]) [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" diff --git a/frontend/src/app/main/ui/workspace/viewport/gl.cljs b/frontend/src/app/main/ui/workspace/viewport/gl.cljs index 2157977f81..3fdba7479f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gl.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gl.cljs @@ -2,87 +2,129 @@ (:require-macros [app.main.style :as stl]) (:require-macros [app.util.gl.macros :refer [slurp]]) (:require + ["gl-matrix" :as glm] [app.common.math :as math] [app.util.gl :as gl] [cuerdas.core :as str] [rumext.v2 :as mf])) - (def CANVAS_CONTEXT_ID "webgl2") +(def CANVAS_CONTEXT_ID "webgl2") - (def default-vertex-shader (slurp "src/app/util/gl/shaders/default.v.glsl")) - (def default-fragment-shader (slurp "src/app/util/gl/shaders/default.f.glsl")) +(def default-vertex-shader (slurp "src/app/util/gl/shaders/default.v.glsl")) +(def default-fragment-shader (slurp "src/app/util/gl/shaders/default.f.glsl")) - #_(def shaders (js/Map.)) - (def programs (js/Map.)) - #_(def textures (js/Map.)) - #_(def framebuffers (js/Map.)) +#_(def shaders (js/Map.)) +(def programs (js/Map.)) +#_(def textures (js/Map.)) +#_(def framebuffers (js/Map.)) - (defn parse-color-hex - [color opacity] - (let [r (str/slice color 1 3) - g (str/slice color 3 5) - b (str/slice color 5 7)] - #js [(/ (js/parseInt r 16) 255.0) - (/ (js/parseInt g 16) 255.0) - (/ (js/parseInt b 16) 255.0) - opacity])) +(defn parse-color-hex + "Parses a color string and returns a vector with the RGBA values." + [color opacity] + (let [r (str/slice color 1 3) + g (str/slice color 3 5) + b (str/slice color 5 7)] + #js [(/ (js/parseInt r 16) 255.0) + (/ (js/parseInt g 16) 255.0) + (/ (js/parseInt b 16) 255.0) + opacity])) - (defn parse-color - [color opacity] - (cond - (str/starts-with? color "#") - (parse-color-hex color opacity) +(defn parse-color + "Parses a color string and returns a vector with the RGBA values." + [color opacity] + (cond + (str/starts-with? color "#") + (parse-color-hex color opacity) - :else - #js [0.0 0.0 0.0 1.0])) + :else + #js [0.0 0.0 0.0 1.0])) - (defn resize-canvas-to - [canvas width height] - (let [resized-width (not= (.-width canvas) width) - resized-height (not= (.-height canvas) height) - resized (or resized-width resized-height)] - (when resized-width - (set! (.-width canvas) width)) - (when resized-height - (set! (.-height canvas) height)) - resized)) +(defn get-object-type-as-int + "Returns the object type as an integer." + [object] + (case (:type object) + :rect 0 + :circle 1 + :group 2 + :path 3 + :text 4 + :image 5 + :svg-raw 6 + :bool 7 + :frame 8)) - (defn resize-canvas - [canvas] - (let [width (math/floor (.-clientWidth canvas)) - height (math/floor (.-clientHeight canvas))] - (resize-canvas-to canvas width height))) +(defn resize-canvas-to + "Resize canvas to specific coordinates." + [canvas width height] + (let [resized-width (not= (.-width canvas) width) + resized-height (not= (.-height canvas) height) + resized (or resized-width resized-height)] + (when resized-width + (set! (.-width canvas) width)) + (when resized-height + (set! (.-height canvas) height)) + resized)) - (defn prepare-gl - [gl] - (let [default-program (gl/create-program-from-sources gl default-vertex-shader default-fragment-shader)] - (.set programs "default" default-program))) +(defn resize-canvas + "Resizes the canvas intrinsic size to the element size." + [canvas] + (let [width (math/floor (.-clientWidth canvas)) + height (math/floor (.-clientHeight canvas))] + (resize-canvas-to canvas width height))) + +(defn prepare-gl + "Prepares the WebGL context for rendering." + [gl] + (let [default-program (gl/create-program-from-sources gl default-vertex-shader default-fragment-shader)] + (.set programs "default" default-program))) (defn render-gl + "Renders the whole document to the canvas." [gl objects vbox] - (.clearColor gl 1.0 0.0 1.0 0) - (.clear gl (.-COLOR_BUFFER_BIT gl)) + (let [projection (.create glm/mat3) + projection (.projection glm/mat3 projection (:width vbox) (:height vbox))] - (.viewport gl 0 0 (.-width (.-canvas gl)) (.-height (.-canvas gl))) + (.clearColor gl 1.0 0.0 1.0 0.5) + (.clear gl (.-COLOR_BUFFER_BIT gl)) - (.useProgram gl (.get programs "default")) - (.uniform4f gl (.getUniformLocation gl (.get programs "default") "u_vbox") (:x vbox) (:y vbox) (:width vbox) (:height vbox)) + (.viewport gl 0 0 (.-width (.-canvas gl)) (.-height (.-canvas gl))) - (.enable gl (.-BLEND gl)) - (.blendFunc gl (.-SRC_ALPHA gl) (.-ONE_MINUS_SRC_ALPHA gl)) + ;; Enable alpha blending + (.enable gl (.-BLEND gl)) + (.blendFunc gl (.-SRC_ALPHA gl) (.-ONE_MINUS_SRC_ALPHA gl)) - (doseq [[_ object] objects] - (let [selrect (:selrect object) - x (:x selrect) - y (:y selrect) - width (:width selrect) - height (:height selrect)] - (doseq [fill (reverse (:fills object))] - (do - (.uniform4fv gl (.getUniformLocation gl (.get programs "default") "u_color") (parse-color (:fill-color fill) (:fill-opacity fill))) - (.uniform2f gl (.getUniformLocation gl (.get programs "default") "u_size") width height) - (.uniform2f gl (.getUniformLocation gl (.get programs "default") "u_position") x y) - (.drawArrays gl (.-TRIANGLE_STRIP gl) 0 4)))))) + (.useProgram gl (.get programs "default")) + (println "---------------> vbox" (:x vbox) (:width vbox) (:y vbox) (:height vbox)) + (.uniformMatrix3fv gl (.getUniformLocation gl (.get programs "default") "u_projection") false projection) + (.uniform4f gl (.getUniformLocation gl (.get programs "default") "u_vbox") (:x vbox) (:y vbox) (:width vbox) (:height vbox)) + + (doseq [[_ object] objects] + (let [selrect (:selrect object) + x (:x selrect) + y (:y selrect) + width (:width selrect) + height (:height selrect) + rotation (:rotation object) + ;; Tengo que encontrar la forma de "reordenar la matriz" para que funcione la + ;; rotación. + ;; transform (:transform object) + ;; {a :a b :b c :c d :d e :e f :f} transform + ;; matrix #_(js/Float32Array. #js [a c 0 b d 0 0 0 1]) + matrix (js/Float32Array. #js [1 0 0 0 1 0 0 0 1]) + fill (first (:fills object))] + (js/console.log "fill" fill) + (js/console.log "matrix" matrix) + (.uniform1i gl (.getUniformLocation gl (.get programs "default") "u_type") (get-object-type-as-int object)) + (.uniform2f gl (.getUniformLocation gl (.get programs "default") "u_size") width height) + (.uniform2f gl (.getUniformLocation gl (.get programs "default") "u_position") x y) + (.uniform1f gl (.getUniformLocation gl (.get programs "default") "u_rotation") (/ (* rotation js/Math.PI) 180.0)) + #_(.uniformMatrix3fv gl (.getUniformLocation gl (.get programs "default") "u_transform") false matrix) + ;; NOTA: Esto es sólo aplicable en objetos que poseen fills (los textos no + ;; poseen fills). + (doseq [fill (reverse (:fills object))] + (do + (.uniform4fv gl (.getUniformLocation gl (.get programs "default") "u_color") (parse-color (:fill-color fill) (:fill-opacity fill))) + (.drawArrays gl (.-TRIANGLE_STRIP gl) 0 4))))))) (mf/defc canvas "A canvas element with a WebGL context." @@ -91,7 +133,21 @@ (let [objects (unchecked-get props "objects") vbox (unchecked-get props "vbox") canvas-ref (mf/use-ref nil) - gl-ref (mf/use-ref nil)] + gl-ref (mf/use-ref nil) + + on-context-lost + (mf/use-fn (fn [] + (mf/set-ref-val! gl-ref nil))) + + on-context-restore + (mf/use-fn (fn [] + (let [canvas (mf/ref-val canvas-ref)] + (when (some? canvas) + (let [gl (.getContext canvas CANVAS_CONTEXT_ID)] + (mf/set-ref-val! gl-ref gl) + (resize-canvas canvas) + (prepare-gl gl) + (render-gl gl objects vbox))))))] (mf/with-effect [objects vbox] (let [gl (mf/ref-val gl-ref)] @@ -101,15 +157,24 @@ (mf/with-effect [canvas-ref] (let [canvas (mf/ref-val canvas-ref)] (when (some? canvas) + (.addEventListener canvas "webglcontextlost" on-context-lost) + (.addEventListener canvas "webglcontextrestore" on-context-restore) (let [gl (.getContext canvas CANVAS_CONTEXT_ID)] (mf/set-ref-val! gl-ref gl) (resize-canvas canvas) (prepare-gl gl) - (render-gl gl objects vbox))))) + (render-gl gl objects vbox)))) + + ;; unmount + (fn [] + (let [canvas (mf/ref-val canvas-ref)] + (when (some? canvas) + (.removeEventListener canvas "webglcontextlost" on-context-lost) + (.removeEventListener canvas "webglcontextrestore" on-context-restore))))) [:canvas {:class (stl/css :canvas) :ref canvas-ref}])) -;; TODO +;; TODO ;; - blend modes -;; - strokes \ No newline at end of file +;; - strokes diff --git a/frontend/src/app/util/gl/shaders/default.f.glsl b/frontend/src/app/util/gl/shaders/default.f.glsl index d950503467..af2abec808 100644 --- a/frontend/src/app/util/gl/shaders/default.f.glsl +++ b/frontend/src/app/util/gl/shaders/default.f.glsl @@ -1,18 +1,50 @@ #version 300 es +const int type_rect = 0; +const int type_circle = 1; +const int type_group = 2; +const int type_path = 3; +const int type_text = 4; +const int type_image = 5; +const int type_svg_raw = 6; +const int type_bool = 7; +const int type_frame = 8; + precision highp float; -// in vec2 v_texCoord; - -// uniform sampler2D u_texture; -uniform vec4 u_color; - out vec4 fragColor; +in vec2 v_texCoord; + +uniform int u_type; +uniform vec4 u_color; + void main() { - // fragColor = texture(u_framebuffer, v_texCoord) + texture(u_texture, v_texCoord); - // fragColor = texture(u_texture, v_texCoord); - // Pintamos rosita - fragColor = u_color; - // fragColor = vec4(1.0, 0.0, 1.0, 1.0); + // Si es un rect o un frame, simplemente asignamos el color al fragColor. + if (u_type == type_rect || u_type == type_frame) { + fragColor = u_color; + // Si es un circulo, comprobamos que el pixel este dentro del circulo, en caso + // contrario descartamos el pixel. + } else if (u_type == type_circle) { + if (length(v_texCoord - 0.5) > 0.5) { + discard; + } + if(length(v_texCoord - 0.5f) > 0.45f) { + + fragColor = vec4(1.0, 0.0, 0.0, 1.0); + } else if(length(v_texCoord - 0.5f) > 0.4f) { + fragColor = vec4(1.0f, 0.0f, 1.0f, 1.0f); + } else { + fragColor = u_color; + } + // Para cualquier otro elemento no soportado pintamos una especie de rejilla + // raruna. + } else { + fragColor = vec4( + round(mod(v_texCoord.x, 0.1) * 10.0), + round(mod(v_texCoord.y, .1) * 10.0), + 0.0, + 1.0 + ); + } } \ No newline at end of file diff --git a/frontend/src/app/util/gl/shaders/default.v.glsl b/frontend/src/app/util/gl/shaders/default.v.glsl index c465999ef6..53cc8ffdde 100644 --- a/frontend/src/app/util/gl/shaders/default.v.glsl +++ b/frontend/src/app/util/gl/shaders/default.v.glsl @@ -5,47 +5,46 @@ precision highp float; uniform vec4 u_vbox; uniform vec2 u_position; uniform vec2 u_size; +uniform float u_rotation; +uniform mat3 u_projection; -// out vec2 v_texCoord; +out vec2 v_texCoord; vec2 get_vertex_position(int id) { if(id == 0) { - return vec2(0.0f, 0.0f); + return vec2(-1.0, -1.0); } else if(id == 1) { - return vec2(1.0f, 0.0f); + return vec2(1.0, -1.0); } else if(id == 2) { - return vec2(0.0f, 1.0f); + return vec2(-1.0, 1.0); } else if(id == 3) { - return vec2(1.0f, 1.0f); + return vec2(1.0, 1.0); } else { return vec2(0.0f, 0.0f); } } -/* -vec2 get_tex_position(int id) { - if(id == 0) { - return vec2(0.0f, 1.0f); - } else if(id == 1) { - return vec2(1.0f, 1.0f); - } else if(id == 2) { - return vec2(0.0f, 0.0f); - } else if(id == 3) { - return vec2(1.0f, 0.0f); - } else { - return vec2(0.0f, 0.0f); - } -} -*/ - -vec2 from(vec2 v, vec2 min, vec2 max) { - return (v - min) / (max - min); -} - void main() { + vec2 center = u_size * 0.5; + vec2 position = u_position - vec2(u_vbox.xy); + + float c = cos(u_rotation); + float s = sin(u_rotation); + + mat2 rotation = mat2(c, s, -s, c); + mat2 scale = mat2( + u_size.x * 0.5, 0.0f, + 0.0f, u_size.y * 0.5 + ); + mat2 rotation_scale = rotation * scale; + vec2 vertex = get_vertex_position(gl_VertexID); - vec2 position = vertex * from(u_size, vec2(0), u_vbox.zw) + from(u_position, u_vbox.xy, u_vbox.xy + u_vbox.zw); // 0,1 - gl_Position = vec4(mix(vec2(-1.0, 1.0), vec2(1.0, -1.0), position), 0.0f, 1.0f); - // gl_Position = vec4(((get_base_position(gl_VertexID) * u_size + u_position) / u_vbox.zw), 0.0f, 1.0f); - // v_texCoord = get_tex_position(gl_VertexID); + + vec2 vertex_rotated_scaled = rotation_scale * vertex; + vec2 vertex_positioned = center + vertex_rotated_scaled + position; + + vec3 projected = u_projection * vec3(vertex_positioned, 1.0); + + gl_Position = vec4(projected, 1.0f); + v_texCoord = (vertex + 1.0) * 0.5; } \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f50dbb9699..63875f092d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7587,6 +7587,7 @@ __metadata: draft-js: "npm:^0.11.7" eventsource-parser: "npm:^1.1.1" gettext-parser: "npm:^7.0.1" + gl-matrix: "npm:^3.4.3" gulp: "npm:4.0.2" gulp-cached: "npm:^1.1.1" gulp-concat: "npm:^2.6.1" @@ -7891,6 +7892,13 @@ __metadata: languageName: node linkType: hard +"gl-matrix@npm:^3.4.3": + version: 3.4.3 + resolution: "gl-matrix@npm:3.4.3" + checksum: c8ee6e2ce2d089b4ba4ae13ec9d4cb99bf2abe5f68f0cb08d94bbd8bafbec13aacc7230b86539ce5ca01b79226ea8c3194f971f5ca0c81838bc5e4e619dc398e + languageName: node + linkType: hard + "glob-parent@npm:^3.1.0": version: 3.1.0 resolution: "glob-parent@npm:3.1.0"