Skip to content

Instantly share code, notes, and snippets.

@cstorey
Created February 24, 2014 22:50
Show Gist options
  • Save cstorey/9198985 to your computer and use it in GitHub Desktop.
Save cstorey/9198985 to your computer and use it in GitHub Desktop.
A trivial clojurescript paint program using Reagent and core.async. Greatly inspired by the RxJS paint example.
<html>
<head>
<script src="out/goog/base.js" type="text/javascript"></script>
<script src="hello_world.js" type="text/javascript"></script>
<script type="text/javascript">goog.require("hello_world.core");</script>
</head>
<body>
</body>
<script type="text/javascript">hello_world.core.run()</script>
</html>
(defproject hello-world "0.1.0-SNAPSHOT"
:description "FIXME: write this!"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.5.1"]
[org.clojure/clojurescript "0.0-2173"]
[reagent "0.4.1"]
[org.clojure/core.async "0.1.267.0-0d7780-alpha"]
[org.clojure/core.match "0.2.1"]]
:plugins [[lein-cljsbuild "1.0.2"]]
:source-paths ["src"]
:cljsbuild {
:builds [{:id "hello-world"
:source-paths ["src"]
:compiler {
:preamble ["reagent/react.js"]
:output-to "hello_world.js"
:output-dir "out"
:optimizations :whitespace
:source-map "hello_world.js.map"}}]})
; Actual path: src/hello_world/core.cljs
(ns hello-world.core
(:require [reagent.core :as reagent :refer [atom]]
[cljs.core.async :as async
:refer [<! >! chan close! sliding-buffer put! alts!]]
[cljs.core.match])
(:require-macros [cljs.core.async.macros :refer [go go-loop]]
[cljs.core.match.macros :refer [match]]))
(enable-console-print!)
(defn controller [inchan state-ref]
(go-loop [cur-x nil cur-y nil mouse-state :up]
(match [(<! inchan) mouse-state]
[({:type "mousedown" :x x :y y} :as e) :up]
(do
(recur x y :down))
[({:type "mousemove" :x x :y y} :as e) :down]
(do
(swap! state-ref update-in [:current-path] conj {:x x :y y})
(recur x y :down))
[({:type "mouseup" :x x :y y} :as e) :down]
(do
(swap! state-ref (fn [{:keys [current-path paths] :as state}]
(assoc state :paths (conj paths current-path) :current-path [])))
(recur x y :up))
[s e] (recur cur-x cur-y mouse-state))))
(defn path-component [path fill-color]
(let [xys (map (fn [{:keys [x y]}] (str x " " y)) path)
points (apply str (interpose ", " xys))]
[:polyline {:points points :stroke fill-color :fill "none"}]))
(defn event-handler-fn [comms component e]
(let [bounds (. (reagent/dom-node component) getBoundingClientRect)
x (- (.-pageX e) (.-top bounds))
y (- (.-pageY e) (.-left bounds))]
(put! comms {:type (.-type e) :x x :y y})))
(defn hello-world-app [{:keys [state-ref comms] :as props}]
(let [{:keys [paths current-path]} @state-ref
component (reagent/current-component)
handler-fn (partial event-handler-fn comms component)]
[:svg {:height 480 :width 640
:on-mouse-up handler-fn :on-mouse-down handler-fn :on-mouse-move handler-fn
:style {:border "thin solid black"}}
(cons [path-component current-path "red"]
(map #(vector path-component % "black") paths))]))
(defn ^:export run []
(let [paths-atom (atom {:paths []})
ch (chan)
proc (controller ch paths-atom)]
(reagent/render-component
[hello-world-app {:state-ref paths-atom :comms ch}]
(.-body js/document))
(go
(let [exit (<! proc)]
(prn :exit! exit)))))
@scgilardi
Copy link

excellent example. thank you.

re: event-handler-fn, the ".-top" and ".-left" are swapped: x should subtract off the left and y should subtract off the top. Also, the bounds rect itself is in "client" coordinates (getBoundingCientRect). When the enclosing view is a div that might be scrolled, using .-clientX and .-clientY (rather than .-page*) worked properly for me.

@chrismurrph
Copy link

Short but great for showing how to do something graphical and interactive with Reagent/React. I have made it work with Figwheel and put it in a project here. My intention being to morph it into an interactive graphical display of values over time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment