Talking to Three.js from CLJS
Original Pen
This is a port of the excellent pen by ycw into clojurescript. Three.js has an API
which is all about mutation and chaining (check all the set
, get
calls!), which makes it particularly challenging to
port directly to CLJS.
ycw also used a fun technique, where they defined a custom property $x
on the groups of meshes, allowing gsap to read and
overwrite this position via a single property, even though the value is held and written in a nested property of the group.
First things first.
Let's import all the libraries we'll be using from skypack and expose them to window
so CLJS can
get its hands on them. As far as I know, there isn't a good way to do this just yet directly from cljs..? I guess we could
use the dynamic import(url).then(...)
API as well, but can leave that for next time - would be an interesting challenge on its own.
<script type="module">
import * as $3 from 'https://cdn.skypack.dev/three@0.125.0/build/three.module.js?min';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/controls/OrbitControls.js?min';
import { EffectComposer } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/EffectComposer.js?min';
import { RenderPass } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/RenderPass.js?min';
import { UnrealBloomPass } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/UnrealBloomPass.js?min';
import { gsap } from 'https://cdn.skypack.dev/gsap?min';
window.libs = {
$3,
OrbitControls,
EffectComposer,
RenderPass,
UnrealBloomPass,
gsap
};
</script>
Start doing things.
(ns sketch.core
;; Klipse has some magic where it loads applied science's interop library
;; somehow, just by being here. Cool!
(:require [applied-science.js-interop :as j]))
(defn sketch []
;; Boot
(let [{:keys [OrbitControls
EffectComposer
RenderPass
UnrealBloomPass
gsap
$3]} (j/lookup (j/get js/window :libs))
el (js/document.getElementById "mc")
renderer ($3.WebGLRenderer. #js {:canvas el
:antialias true})
scene ($3.Scene.)
camera ($3.PerspectiveCamera. 75 2 0.1 1000)
controls (OrbitControls. camera el)]
(js/window.addEventListener "resize"
(fn []
(let [w (.-clientWidth el)
h (.-clientHeight el)]
(doto renderer
(.setSize w h false)
(.setPixelRatio (.-devicePixelRatio js/window)))
(doto camera
(j/assoc! :aspect (/ w h))
(.updateProjectionMatrix)))))
(.dispatchEvent js/window (js/window.Event. "resize"))
;; Setup
(j/call-in camera [:position :set] -2 8 -2)
(let [steps 1000
extrude-steps (* 1024 2)
extrude-depth 0.5
half-extrude-depth (/ extrude-depth 2)
thickness 1
half-thickness (/ thickness 2)
repetition 4
scale-x 100
light0 (doto ($3.DirectionalLight. "white" 1)
(j/call-in [:position :set] 2 -2 -2))
_ (j/call scene :add light0)
light1 (doto ($3.DirectionalLight. "white" 1)
(j/call-in [:position :set] -2 2 2))
_ (j/call scene :add light1)
curve ($3.CurvePath.)
shape ($3.Shape.)]
(loop [i 0
prev ($3.Vector3.)
curr ($3.Vector3.)]
(let [t (/ i steps)
ft (js/Math.sin (* t
(.-PI js/Math)
2
repetition))
x (* scale-x (- t 0.5))
y ft]
(if (zero? i)
(do
(j/call prev :set x y 0)
(recur (inc i)
prev
curr))
(when (< i steps)
(let [curr ($3.Vector3. x y 0)
line ($3.LineCurve3. prev curr)]
(j/call curve :add line)
(recur (inc i)
curr
nil))))))
(doto shape
(j/call :moveTo (- half-extrude-depth) (- half-thickness))
(j/call :lineTo (- half-extrude-depth) half-thickness)
(j/call :lineTo half-extrude-depth half-thickness)
(j/call :lineTo half-extrude-depth (- half-thickness))
(j/call :lineTo (- half-extrude-depth) (- half-thickness)))
(let [geom ($3.ExtrudeGeometry. shape #js {:extrudePath curve
:bevelEnabled false
:steps extrude-steps})
gs (reduce (fn [acc i]
(let [I 16
color (doto ($3.Color.)
(j/call :setHSL (* 2 (/ i I)) 0.8 0.5))
mat ($3.MeshPhongMaterial. #js {:color color})
mesh ($3.Mesh. geom #js [nil mat])
g ($3.Group.)]
(j/call g :add mesh)
(js/Object.defineProperty
g
"$x"
#js {:get #(j/get-in g [:position :x])
:set #(j/assoc-in! g [:position :x] %)})
(j/assoc-in! mesh [:position :z] (-> i
(/ (- I 1))
(- 0.5)
(* 20)))
(j/assoc-in! mesh [:position :x] (mod i 8))
(j/call scene :add g)
(j/call acc :push g)
acc))
#js []
(range 16))
res (j/call renderer :getDrawingBufferSize ($3.Vector2.))
composer (EffectComposer. renderer)]
(j/call js/window :addEventListener "resize"
(fn []
(let [{:keys [clientWidth clientHeight]} (j/lookup el)]
(j/call renderer :getDrawingBufferSize res)
(j/call composer :setPixelRatio (j/get js/window :devicePixelRatio))
(j/call composer :setSize clientWidth clientHeight))))
(doto composer
(j/call :addPass (RenderPass. scene camera))
(j/call :addPass (UnrealBloomPass. res 1 0.5 0.1)))
(j/call renderer :setAnimationLoop (fn []
(j/call composer :render)
(j/call controls :update)))
(j/call gsap :to gs #js {:$x (/ scale-x repetition)
:duration 1
:ease "none"
:repeat -1})))
:done))
(sketch)