edu.kit.ieb.blm/varitory

0.0.1-21


Projekt um die Möglichkeiten einer Anwendungsarchitektur zu skizzieren.

dependencies

org.clojure/clojure
1.7.0
org.clojure/clojurescript
1.7.48
org.clojure/core.async
0.2.374
kioo
0.5.0-20160228.154836-1
prismatic/dommy
1.1.0
com.taoensso/timbre
4.3.1
cljsjs/bootstrap
3.3.6-0
cljsjs/bootstrap-slider
7.0.1-0
cljsjs/marked
0.3.5-0
cljs-ajax
0.5.4



(this space intentionally left almost blank)
 
(ns edu.kit.ieb.blm.macros)
(defmacro once [ref & body]
  `(let [called# ~ref]
     (fn [] (when-not @called# (do ~@body (reset! called# true))))))
 

Namensraum als Einstiegspunkt zum Laden der Anwendung

(ns edu.kit.ieb.blm.varitory.app
  (:require [edu.kit.ieb.blm.varitory.view :as view]
            [edu.kit.ieb.blm.varitory.behavior]))

Wird typischer Weise vom onLoad Event des body Tags der Website aufgerufe, um die Anwendung zu initiieren.

(defn ^:export init
  []
  (view/init))
 

Dieser Namensraum beinhalten die Logik der Anwendung.

Auf die Funktionen kann direkt aus der View heraus zugegriffen werden. Ansonsten hat auch dieser Namensraum nur direkten Zugriff auf die States der Anwendung.

(ns edu.kit.ieb.blm.varitory.behavior
  (:require [edu.kit.ieb.blm.varitory.state :as state]
            [cljs.core.async :as async]
            [dommy.core :refer-macros [sel1]])
  (:require-macros [cljs.core.async.macros :refer [go]]))

Die einfachste Art in der Historie zu Wandern, ist eine vergangenen Stand zu nehmen und einfach den aktuellen State damit zu übeschreiben. An dieser Stelle wird sehr deutlich welches Potential diese Anwendungsarchitektur hat.

Da sich die View immer als Abbildung des View Modell beschreiben lässt. Ist kein aufwendiges zurücksetzen der View nötigt. Es reicht die Parameter zu ändern.

(defn jump!
  [value]
  (reset! state/state (get @state/history value)))

Hier wird für den übergebene Text der State des View Modells aktuallisiert und ein neuer Stand im State der Historie hinterlegt. Diese Funktion ist ein entscheidener Punkt, um zu konktrolliere, welche Stände gespeichert werden.

(defn- update-history!
  [value selection]
  (let [new-state (reset! state/state (state/new-state value selection))]
    (swap! state/history #(conj % new-state))))

Diese Funktion änder den State des View Modells ohne die Historie zu verändern. Sie wird auch genutzt um Events zur Navigation zu verarbeiten.

(defn ^:export onFocus
  [textarea]
  (state/update-cursor! (.-value textarea) (.-selectionStart textarea)))

Sobald der Nutzer eine Eingabe im Textarea Backend durchführt, muss die entsprechend Darstellung im x3d Frontend akktualisiert werden. Um die Perfromanze zu steigern wird dabei, zwischen Events, die zur Navigation in der Textarea dienen sowie Events die den Inhalt änder unterschieden.

(defn ^:export onKeyUp
  [event]
  (let [textarea (.-target event)]
    (case (.-keyIdentifier event)
      "Up" (onFocus textarea)
      "Down" (onFocus textarea)
      "Left" (onFocus textarea)
      "Right" (onFocus textarea)
      (update-history! (.-value textarea)
                       (.-selectionStart textarea)))))
 

Dieser Namensraum hält den Zustand (State) der Anwendung.

Ein State kann in diesem Kontext analog als View Modell bezeichnet werden, da die View nur auf Grundlage eines States ihre Inhalte zeichnet.

Der Namensraum beinhaltet neben, den transienten Referenzen der States, die in der Laufzeit der Anwendung als einzige veränderlich sind, auch Funktionen, um diese States zu manipulieren.

(ns edu.kit.ieb.blm.varitory.state
  (:require [clojure.string :as string]))

Anhande eines Textes und einem Index für die Cursor Position, wird ein String erzeugt der alle Zeichen des Textes durch Leerzeichen ersetzt und nur an der Stelle der Cursor Position Unterstrich einführt.

Dadurch kann bei Monospace Fonts ein Cursor einfach dadurch realisiert werden, dass dieser String in einem leicht verschobenen weitere Text Element gerendert wird. Quelle

(defn- text->cursor-string
  [text cursor-position]
  (str
   (string/replace
    (apply str (first (split-at cursor-position text)))
    #"[^\n]" " ") "_"))

Um ein Mehrzeiligen Text in x3d zu Rendern, muss jede Zeile durch Quotes von einander unterschieden werden. Diese Funktion nimmt ein Text und wandelt diesen in genau diese Form um, sodass er direkt in einem x3d Text Node genutzt werden kann. Quelle

(defn- prepare
  [text]
  (let [res (reduce
             #(string/replace %1 (first %2) (second %2))
             ;; wenn " quotiert wird, wird jedes weitere quote
             ;; in dieser Ziele mit \" angezeigt
             text [[#"\"" "'"]
                   [#"[\n]" "\"\""]])]
    (str  "\"" res "\"")))

Diese Funktion beschreibt das Verfahren, nach dem die geometrischen Features für die spätere Darstellung berechnet werden. Es ist der zentrale Teil des View Modells.

(defn- feature
  [text]
  (let [f (frequencies (.toLowerCase text))]
    (doto {:color {:ground (case (get f \g) 1 "#8A4B08" 2 "#00BFFF" "#04B404")
                   :house (let [v (max 0.4 (- 1.0 (/ (get f \h 0) 5)))] (str v "," v "," v))
                   :roof (case (get f \r) 1 "#FFFF00" 2 "#3A01DF" "#DF0101")}
           :display (case (get f \a 0) 0 :nothing 1 :ground 2 :house :all)
           :scale (let [v (min 1.5 (+ 1.0 (/ (get f \e 0) 5)))] (str v "," v "," v))
           :rotation (let [v (case (get f \d) 1 0.79 2 1.57 3 2.36 0.0)] (str "0,-1,0," v))
           :details (set (remove nil? [(when (get f \b) :tree)]))})))
(defn- new-state [text cursor-position]
  {:text text
   :editor {:text (prepare text)
            :cursor (prepare (text->cursor-string text cursor-position))}
   :lines (map feature (re-seq #".*[^\n]" text))})

Der View State der Anwendung, der direkt genutzt wird, um daraus die aktuelle Darstellung der x3d Szene abzuleiten.

(defonce state (atom (new-state " " 0)))

Nur der Cursor wird anhand des aktuellen Inhalts und der Selektion des Nutzer aktualisiert. Das spart wahrscheinlich ein wenig des Berechnungsaufwands.

(defn update-cursor!
  [text cursor-position]
  (swap! state #(assoc-in % [:editor :cursor] (prepare (text->cursor-string text cursor-position)))))

Der History State der Anwendung, enthällt alle seit starten der Laufzeit, erzegten View States. Dadurch kann einfach auf einen alten View State Stand zurückgesprungen werden.

(defonce history (atom []))
 

Namensraum enthällt die vom State abgeleitete View.

Neben den Funktione zum Erzeugen der DOM Elemente sind auch einige wenige Funktionen implementiert, die nicht über einen State abgebildet werden.

Hier wird auch das Ladenverhalten von x3dom überwacht und nach erfolgreicher Initialiserung die View Elemente aktiviert und Listener für z.B. das automatische Anpassen beim Wechsel der Fenstergröße angemeldet.

(ns edu.kit.ieb.blm.varitory.view
  (:require [cljsjs.bootstrap] [cljsjs.bootstrap-slider]
            [x3dom.js] [cljsjs.marked]
            [edu.kit.ieb.blm.varitory.state :refer [state history]]
            [edu.kit.ieb.blm.varitory.behavior :refer [jump!]]
            [taoensso.timbre :refer-macros [info debug error]]
            [om.core :as om] [om.dom :as dom] [ajax.core :refer [GET]]
            [goog.events :as events]
            [goog.events.EventType :as EventType]
            [cljs.core.async :refer [timeout <!]]
            [kioo.om
             :refer [content set-attr do-> substitute listen]
             :refer-macros [snippet defsnippet]]
            [dommy.core
             :refer [set-html! set-style! set-attr! show!]
             :refer-macros [sel1]])
  (:import [goog.dom ViewportSizeMonitor])
  (:require-macros [edu.kit.ieb.blm.macros :refer [once]]
                   [net.cgrand.enlive-html :refer [any-node]]
                   [cljs.core.async.macros :refer [go]]))

Private Referenz auf das DOM Element, dass die x3d Szene enthällt.

(def ^:private x3d-node
  (sel1 :x3d))

Private Referenz auf das Textarea Element, das als Backend fungiert.

(def ^:private textarea-node
  (sel1 :textarea))

Mit einem kleinen Delay wird der Viewport so eingestellt, dass das gesamte 3D Modell zu sehen ist. Der Delay ist hier doch notwendig, da es einen Augenblick dauert, bis die Änderungen am State wirklich in der x3dom Laufzeit angekommen sind.

(defn ^:export showAll
  []
  (go (<! (timeout 50))
      (.showAll (.-runtime x3d-node))))

Initialisierung nachdem die x3dom Laufzeit fertig geladen ist.

(set!
 (.-ready (.-runtime js/x3dom))
 (fn []
   (debug "x3dom initialized... setup viewport")
   (let [vm (ViewportSizeMonitor.)
         ;; Sobald sich die Fenstergröße ändern, muss auch der
         ;; Canvas angepasst, werden, damit die maximale größe
         ;; des Fenster ausgenutzt werden kann.
         resize #(-> x3d-node
                     (set-style! :width (str (.-width (.-size_ vm)) "px"))
                     (set-style! :height (str (- (.-height (.-size_ vm)) 120) "px")))]
     (events/listen vm EventType/RESIZE resize)
     ;; Initial wird das Canvas einmal auf die Fenstergröße angepasst.
     (resize)
     ;; Damit der gesamte Initalisierungsprozess nicht zu unansehnlichen
     ;; Ansichten führt, wird das x3d Element erst ganz zum Schluß sichtbar
     ;; geschalten.
     (show! x3d-node)
     (showAll)
     (.focus textarea-node))))

Snippet umfasst den x3d Node, der eine Textebene darstellen soll. Da dieses sowohl für den Text als auch den Cursor genutzt wird, kann ein offset Angegeben werden (Cursor) und eine Identifikator für das Material, damit dieses annimiert werden kann (Cursor).

index.html

(defsnippet textarea  [:.textarea :> any-node]
  [y-offset content material-def]
  {[:transform] (set-attr :translation (str "0," y-offset  ",0"))
   [:material] (if material-def (set-attr :def material-def) identity)
   [:text] (set-attr :string content)})

Snippet umfasst den x3d Node, der ein Variante des Häuschens darstellt. Dir Regeln zum ein und Ausblenden, sind an die im State angepasst.

index.html

(defsnippet site  [:.site]
  [{:keys [color display scale
           rotation details]} x y]
  {[:.site] (if (= :nothing display) (substitute "")
                (set-attr :scale scale
                          :translation (str (* -7 x) "," (* -7 y) ",0")))
   [:.building] (set-attr :rotation rotation)
   [:.building :.detail] (if (and (:tree details)
                                  (not= :nothing display))
                           (set-attr :use "Tree")
                           (substitute ""))
   [:.house] (if (= :ground display) (substitute "") identity)
   [:.roof] (if (#{:ground :house} display) (substitute "") identity)
   [:.ground :material] (set-attr :diffusecolor (:ground color))
   [:.house :material] (set-attr :diffusecolor (:house color))
   [:.roof :material] (set-attr :diffusecolor (:roof color))})

Snippet, dass den gesamten Inhalt der x3d Szene umfasst. Es wird ein Editor durch zwei zueinander verschobenen Textelemente engedeutet. Eine Ebene enhällt den Inhalt und die andere signalisiert die aktuelle Position des Cursors. Der zweite Node ist die Menge der Variante des Häuschens.

index.html

(defsnippet scene  [:x3d :> any-node]
  [{{text :text cursor :cursor} :editor lines :lines}]
  {[:.textarea] (content (textarea 0 text nil))
   [:.cursor] (content (textarea -0.1 cursor "Blink"))
   [:.town] (content (reduce (fn [res [lines x]]
                               (concat res (map site lines (repeat x) (range 0 3))))
                             [] (map #(list %1 %2) (partition 3 3 nil lines)
                                     (iterate inc 0))))})

Handel um sicher zu gehen, dass die View nur einmal instanziiert wird.

(defonce called-already? (atom false))

Initiieren der View Komponente und verdrahten mit den States. Bei der reinen Verwendung von react Komponenten, währe es nicht notwendig, die IWillUpdate Funktionen mit Seiteneffekt zu implementieren.

(def init
  (once
   called-already?
   (debug "Initializing x3d view...")
   ;; Die folgende Komponente verwendet ein Template, dass direkt
   ;; aus dem deklarativ beschrieben HTML Dokument anbgeleitet ist.
   ;; Um an genau dieser Stellen im Dokument jetzt eine dynamische
   ;; Variante zu erzeugen, muss erst der Inhalt zurückgesetzt werden.
   (set-html! x3d-node )
   ;; Die Komponente hat nur eine Seiteneffekt Implementierung.
   ;; Dabei wird davon ausgegangen, dass die Cursor Position im
   ;; Textarea Element immer dann aktualisiert werden muss, wenn
   ;; dieses Element nicht den Fokus hat. Hätte es den Fokus, dann
   ;; wird die Aktualisierung der Cursor Position schon vom Browser
   ;; implementiert.
   (om/root
    #(reify
       om/IRender
       (render [_]
         (scene %))
       om/IWillUpdate
       (will-update [_ state _]
         (go
           (when (not= (.-activeElement js/document) textarea-node)
             (let [cursor (count (get-in state [:editor :cursor]))]
               (debug "Update textare...")
               (set! (.-value textarea-node) (:text state))
               (set! (.-selectionStart textarea-node) cursor)
               (set! (.-selectionEnd textarea-node) cursor)))))
       om/IDidUpdate
       (did-update [_ _ _]
         (showAll)))
    state {:target x3d-node})
   (debug "Initilizing history slider...")
   ;; Das Slider Element dient zum zurückspringen in der Historie.
   ;; Er kann deklarativ beschrieben werden und durch den Aufruf,
   ;; von Behavior Funktionen, ist er trotzdem vom State entkoppelt.
   (let [slider (doto (js/Slider.
                       "#history"
                       #js {:reversed false
                            :enabled false
                            :id "historySlider"
                            :focus true})
                  (.on "change" #(jump! (.-newValue %))))]
     ;; Der einzige Grund, warum hier eine quasi virtuelle View
     ;; Komponente erzeugt werden muss, ist er der, dass der Slider
     ;; immmer dann aktualisiert werden muss, wenn sich der State
     ;; der Historie ändert.
     (om/root
      #(reify
         om/IRender
         ;; Das dom/div wird beim advanced kompilieren zu einem
         ;; Konstrukt, dass so nicht funktioniert. Ist aber auch
         ;; egal, da das Rendering eh nur virtuelle ist.
         (render [_] (dom/p nil ))
         om/IWillUpdate
         (will-update [_ state _]
           (go
             (let [versions (dec (count state))]
               (doto slider
                 (.setAttribute "max" versions)
                 (.setValue versions)
                 (.relayout)
                 (.enable))))))
      history {:target (.createElement js/document "div")}))
   ;; Eine nicht ganz im Sinne der Trennung implementierten des
   ;; dynamischen ableiten der About Page. Korrekte müsste auch
   ;; dies im State liegen und hier nur referenziert werden.
   ;; Da es sich jedoch um quasi statischen Inhalt handelt,
   ;; wiederspricht es nicht dem Konzept und verbleibt der
   ;; Einfachheit halber hiet.
   (GET "about.md"
        {:headers {:Cache-Control "max-age=0, no-cache, no-store"
                   :Pragma "no-cache"}
         :handler #(set-html! (sel1 :#doku) (js/marked %))})
   (GET "licenses.md"
        {:headers {:Cache-Control "max-age=0, no-cache, no-store"
                   :Pragma "no-cache"}
         :handler #(set-html! (sel1 :#licenses) (js/marked %))})))
 
(ns om.dom
  (:refer-clojure :exclude [map meta time]))
(def tags
  '[a
    abbr
    address
    area
    article
    aside
    audio
    b
    base
    bdi
    bdo
    big
    blockquote
    body
    br
    button
    canvas
    caption
    cite
    code
    col
    colgroup
    data
    datalist
    dd
    del
    dfn
    div
    dl
    dt
    em
    embed
    fieldset
    figcaption
    figure
    footer
    form
    h1
    h2
    h3
    h4
    h5
    h6
    head
    header
    hr
    html
    i
    iframe
    img
    ins
    kbd
    keygen
    label
    legend
    li
    link
    main
    map
    mark
    menu
    menuitem
    meta
    meter
    nav
    noscript
    object
    ol
    optgroup
    output
    p
    param
    pre
    progress
    q
    rp
    rt
    ruby
    s
    samp
    script
    section
    select
    small
    source
    span
    strong
    style
    sub
    summary
    sup
    table
    tbody
    td
    tfoot
    th
    thead
    time
    title
    tr
    track
    u
    ul
    var
    video
    wbr
    ;; svg
    circle
    g
    line
    path
    polyline
    rect
    svg
    text
    ])
(def tags
  (concat tags
   (clojure.core/map symbol (read-string (slurp "target/om.json")))))
(defn ^:private gen-react-dom-inline-fn [tag]
  `(defmacro ~tag [opts# & children#]
     `(~'~(symbol "js" (str "React.DOM." (name tag))) ~opts# ~@children#)))
(defmacro ^:private gen-react-dom-inline-fns []
  `(do
     ~@(clojure.core/map gen-react-dom-inline-fn tags)))
(gen-react-dom-inline-fns)
(defn ^:private gen-react-dom-fn [tag]
  `(defn ~tag [opts# & children#]
     (.apply ~(symbol "js" (str "React.DOM." (name tag))) nil (cljs.core/into-array (cons opts# children#)))))
(defmacro ^:private gen-react-dom-fns []
  `(do
     ~@(clojure.core/map gen-react-dom-fn tags)))