diff --git a/deps.edn b/deps.edn index 74a8a72..2faaa32 100644 --- a/deps.edn +++ b/deps.edn @@ -9,8 +9,7 @@ ;; clojurescript build tool and dependencies (see also shadow-cljs.edn) {:cljs {:extra-deps {thheller/shadow-cljs {:mvn/version "2.8.109"} appliedscience/js-interop {:mvn/version "0.2.5"} - reagent {:mvn/version "1.0.0-alpha1"}} - :main-opts ["-m" "shadow.cljs.devtools.cli"]} + reagent {:mvn/version "1.0.0-alpha1"}}} ;; packaging the server as a standalone jar for production :uberjar {:extra-deps {luchiniatwork/cambada {:mvn/version "1.0.2"}} diff --git a/resources/public/style.css b/resources/public/style.css index abba1be..8d52446 100644 --- a/resources/public/style.css +++ b/resources/public/style.css @@ -41,10 +41,18 @@ p:first-of-type { margin-top: 24px; } +p:first-child { + margin-top: 0; +} + p:last-of-type { margin-bottom: 24px } +p:last-child { + margin-bottom: 0 +} + button { background: hsl(240, 100%, 70%); border-radius: 4px; @@ -128,3 +136,27 @@ video#capture { transform: scale(-1, 1); border-radius: 4px; } + +#snapshot-preview { + z-index: 100; + background: #fff; + color: hsl(240, 100%, 70%); + border: 2px solid hsl(240, 100%, 70%); + border-radius: 4px; + box-shadow: 0 8px 0 hsla(240, 100%, 70%, 20%); + width: 480px; + max-width: 88%; +} + +#snapshot-preview img { + display: block; + position: relative; + margin: 0 auto 32px; + transform: scale(-1, 1); + max-width: 100%; + height: auto; +} + +#snapshot-preview button { + margin: 4px; +} diff --git a/src/heyarne/all_my_friends/core.cljs b/src/heyarne/all_my_friends/core.cljs index 9248da5..261058d 100644 --- a/src/heyarne/all_my_friends/core.cljs +++ b/src/heyarne/all_my_friends/core.cljs @@ -3,7 +3,6 @@ [reagent.dom :as dom] [heyarne.all-my-friends.views :as views])) - (defn draw-results [ctx _video predictions] ;; remove previous results (.clearRect ctx 0 0 (.. ctx -canvas -width) (.. ctx -canvas -height)) diff --git a/src/heyarne/all_my_friends/facemesh.cljs b/src/heyarne/all_my_friends/facemesh.cljs index 19b8b3e..d44c4bd 100644 --- a/src/heyarne/all_my_friends/facemesh.cljs +++ b/src/heyarne/all_my_friends/facemesh.cljs @@ -83,7 +83,8 @@ canvas (.querySelector container "#result") ctx (.getContext canvas "2d") model (promisify init-model) - stream (promisify init-webcam)] + stream (promisify init-webcam) + halt? (:halt? (r/props this))] (-> (js/Promise.all #js [model stream]) (.then (fn [[model stream]] (println "model and stream initialized") @@ -91,12 +92,13 @@ (set! (.-srcObject video) stream) (set! (.-onloadedmetadata video) (fn [_] - (.play video) (set! (.-width canvas) (.-videoWidth video)) (set! (.-height canvas) (.-videoHeight video)) - ;; detect-faces will continously be called via requestAnimationFrame - ;; on-faces-detected receives the canvas context as first param and detected predictions as second - (detect-faces model video (partial (:on-faces-detected (r/props this)) ctx)))) + (when-not halt? + (.play video) + ;; detect-faces will continously be called via requestAnimationFrame + ;; on-faces-detected receives the canvas context as first param and detected predictions as second + (detect-faces model video (partial (:on-faces-detected (r/props this)) ctx))))) ;; make the initialized variables available for ;; component-did-update (swap! state assoc diff --git a/src/heyarne/all_my_friends/views.cljs b/src/heyarne/all_my_friends/views.cljs index 1f001eb..a4dbe78 100644 --- a/src/heyarne/all_my_friends/views.cljs +++ b/src/heyarne/all_my_friends/views.cljs @@ -1,5 +1,6 @@ (ns heyarne.all-my-friends.views (:require [reagent.core :as r] + [applied-science.js-interop :as j] [heyarne.all-my-friends.facemesh :refer [webcam-facemesh]])) ;; minimalistic re-frame-like event handling @@ -10,7 +11,9 @@ {:welcome/continue (fn [db _] (assoc db :status :running)) :running/snapshot (fn [db result] - (assoc-in db [:snapshots :current] result))}) + (assoc-in db [:snapshots :current] result)) + :running/discard-snapshot (fn [db _] + (update db :snapshots dissoc :current))}) (defn dispatch [[event data]] (when-let [handler (events event)] @@ -36,6 +39,45 @@ Seit der globalen Covid19-Pandemie sind wir alle dazu gezwungen, auf physischen (.drawImage ctx video-elem 0 0 (.-width canvas) (.-height canvas)) (.getImageData ctx 0 0 (.-width canvas) (.-height canvas)))) +(defn draw-path [ctx path] + (.moveTo ctx (aget path 0 0) (aget path 0 1)) + (doseq [[x y] (rest path)] + (.lineTo ctx x y)) + (.stroke ctx)) + +(defn preview [{{:keys [video predictions]} :snapshot}] + (let [canvas (js/document.createElement "canvas") + ctx (.getContext canvas "2d")] + ;; this is most probably not the most beautiful component i have ever written + (set! (.-predictions js/window) predictions) + (set! (.-width canvas) (.-width video)) + (set! (.-height canvas) (.-height video)) + (set! (.-strokeStyle ctx) "pink") + (set! (.-lineWidth ctx) 2) + (set! (.-lineWidth ctx) "round") + #_(.putImageData ctx video 0 0) + (doseq [prediction predictions] + (draw-path ctx (.slice (j/get-in prediction [:annotations :leftEyebrowLower]) 2)) + (draw-path ctx (j/get-in prediction [:annotations :leftEyeLower0])) + (draw-path ctx (j/get-in prediction [:annotations :leftEyeUpper0])) + (draw-path ctx (.slice (j/get-in prediction [:annotations :rightEyebrowLower]) 2)) + (draw-path ctx (j/get-in prediction [:annotations :rightEyeLower0])) + (draw-path ctx (j/get-in prediction [:annotations :rightEyeUpper0])) + (draw-path ctx (.slice (j/get-in prediction [:annotations :lipsUpperOuter]) 3 -3)) + (draw-path ctx (.slice (j/get-in prediction [:annotations :lipsLowerInner]) 3 -3)) + (draw-path ctx (.slice (j/get-in prediction [:annotations :lipsLowerOuter]) 1 -2)) + (draw-path ctx (.concat (j/get-in prediction [:annotations :noseTip]) + (j/get-in prediction [:annotations :midwayBetweenEyes]))) + (draw-path ctx (.slice (j/get-in prediction [:annotations :silhouette]) 5 -5))) + [:img {:src (.toDataURL canvas)}])) + +(defn snapshot [{:keys [current-snapshot]}] + (let [compliment (rand-nth ["Hübschi!" "Sweet." "Nice!" ":)" "Uh lala~"])] + [:div#snapshot-preview.container + [:p compliment] + [preview {:snapshot current-snapshot}] + [:button {:on-click #(println "submit")} "Abschicken"] + [:button {:on-click #(dispatch [:running/discard-snapshot])} "Lieber noch eins"]])) (defn running [{:keys [on-faces-detected halt?]}] (let [result (atom {:video nil @@ -54,13 +96,15 @@ Seit der globalen Covid19-Pandemie sind wir alle dazu gezwungen, auf physischen (defn app [{:keys [on-faces-detected]}] (let [state @state status (:status state) - halt? (some? (get-in state [:snapshots :current]))] + current-snapshot (get-in state [:snapshots :current])] [:<> [welcome-message {:hidden? (not= :welcome-message status)}] + (when current-snapshot + [snapshot {:current-snapshot current-snapshot}]) (case status :permission-rejected [:div "Sad :("] :running [running {:on-faces-detected on-faces-detected - :halt? halt?}] + :halt? (some? current-snapshot)}] ;; default nil)]))