Clojurescript:制作弹跳球的功能方法
Clojurescript: functional way to make a bouncing ball
我正在学习 Clojurescript,同时将其与 Javascript 进行比较并重写一些脚本。
在 Javascript 中,我创建了一个 canvas,里面有一个球,当它到达 canvas 的边界时,它会反弹回来。我在 Clojurescript 中做了同样的事情,它有效,但我需要在函数外创建 atoms
,这样我就可以跟踪状态。如果我想创造更多的球,我将需要复制那些原子。到那时,代码将变得丑陋。我应该如何更改代码才能创建多个球并且每个球都有自己的状态?
这里是 Javascript 代码:
// Circle object
function Circle(pos_x, pos_y, radius, vel_x, vel_y){
// Starting variables
this.radius = radius;
this.pos_x = pos_x;
this.pos_y = pos_y;
this.vel_x = vel_x;
this.vel_y = vel_y;
// Draw circle on the canvas
this.draw = function(){
c.beginPath();
c.arc(this.pos_x, this.pos_y, this.radius, 0, Math.PI * 2, false);
c.strokeStyle = this.color;
c.lineWidth = 5;
c.fillStyle = this.color_fill;
c.fill();
c.stroke();
};
// Update the circle variables each time it is called
this.update = function(){
// Check if it goes out of the width
if(this.pos_x + this.radius > canvas.width || this.pos_x - this.radius < 0){
// Invert velocity = invert direction
this.vel_x = -this.vel_x;
}
// Check if it goies out of the height
if(this.pos_y + this.radius > canvas.height || this.pos_y - this.radius < 0){
this.vel_y = -this.vel_y;
}
// Apply velocity
this.pos_x += this.vel_x;
this.pos_y += this.vel_y;
// Draw circle
this.draw();
};
};
// Create a single circle
let one_circle = new Circle(300, 300, 20, 1, 1);
function animate(){
requestAnimationFrame(animate);
// Clear canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// Update all the circles
one_circle.update();
}
animate();
这是 Clojurescript 代码:
(def ball-x (atom 300))
(def ball-y (atom 300))
(def ball-vel-x (atom 1))
(def ball-vel-y (atom 1))
(defn ball
[pos-x pos-y radius]
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c))
(defn update-ball
[]
(if (or (> (+ @ball-x radius) (.-width canvas)) (< (- @ball-x radius) 0))
(reset! ball-vel-x (- @ball-vel-x)))
(if (or (> (+ @ball-y radius) (.-height canvas)) (< (- @ball-y radius) 60))
(reset! ball-vel-y (- @ball-vel-y)))
(reset! ball-x (+ @ball-x @ball-vel-x))
(reset! ball-y (+ @ball-y @ball-vel-y))
(ball @ball-x @ball-y 20))
(defn animate
[]
(.requestAnimationFrame js/window animate)
(update-ball))
(animate)
编辑:我尝试了一种新的方法来解决这个问题,但这不起作用。球已创建,但它不会移动。
(defrecord Ball [pos-x pos-y radius vel-x vel-y])
(defn create-ball
[ball]
(.beginPath c)
(.arc c (:pos-x ball) (:pos-y ball) (:radius ball) 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c))
(def balls (atom {}))
(reset! balls (Ball. 301 300 20 1 1))
(defn calculate-movement
[ball]
(let [pos-x (:pos-x ball)
pos-y (:pos-y ball)
radius (:radius ball)
vel-x (:vel-x ball)
vel-y (:vel-y ball)
new-ball (atom {:pos-x pos-x :pos-y pos-y :radius radius :vel-x vel-x :vel-y vel-y})]
; Check if out of boundaries - width
(if (or (> (+ pos-x radius) (.-width canvas)) (< (- pos-x radius) 0))
(swap! new-ball assoc :vel-x (- vel-x)))
; Check if out of boundaries - height
(if (or (> (+ pos-y radius) (.-height canvas)) (< (- pos-y radius) 60))
(swap! new-ball assoc :vel-y (- vel-y)))
; Change `pos-x` and `pos-y`
(swap! new-ball assoc :pos-x (+ pos-x (@new-ball :vel-x)))
(swap! new-ball assoc :pos-x (+ pos-y (@new-ball :vel-y)))
(create-ball @new-ball)
(println @new-ball)
@new-ball))
(defn animate
[]
(.requestAnimationFrame js/window animate)
(reset! balls (calculate-movement @balls)))
(animate)
在@Carcigenicate 的帮助下,这是一个有效的脚本。
;;; Interact with canvas
(def canvas (.getElementById js/document "my-canvas"))
(def c (.getContext canvas "2d"))
;;; Set width and Height
(set! (.-width canvas) (.-innerWidth js/window))
(set! (.-height canvas) (.-innerHeight js/window))
(defrecord Ball [pos-x pos-y radius vel-x vel-y])
; Making the atom hold a list to hold multiple balls
(def balls-atom (atom []))
; You should prefer "->Ball" over the "Ball." constructor. The java interop form "Ball." has a few drawbacks
; And I'm adding to the balls vector. What you had before didn't make sense.
; You put an empty map in the atom, then immedietly overwrote it with a single ball
(doseq [ball (range 10)]
(swap! balls-atom conj (->Ball (+ 300 ball) (+ 100 ball) 20 (+ ball 1) (+ ball 1))))
; You called this create-ball, but it doesn't create anything. It draws a ball.
(defn draw-ball [ball]
; Deconstructing here for clarity
(let [{:keys [pos-x pos-y radius]} ball]
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c)))
(defn draw-balls [balls]
(doseq [ball balls]
(draw-ball ball)))
(defn out-of-boundaries
[ball]
"Check if ball is out of boundaries.
If it is, returns a new ball with inversed velocities."
(let [{:keys [pos-x pos-y vel-x vel-y radius]} ball]
;; This part was a huge mess. The calls to swap weren't even in the "if".
;; I'm using cond-> here. If the condition is true, it threads the ball. It works the same as ->, just conditionally.
(cond-> ball
(or (> (+ pos-x radius) (.-width canvas)) (< (- pos-x radius) 0))
(update :vel-x -) ; This negates the x velocity
(or (> (+ pos-y radius) (.-height canvas)) (< (- pos-y radius) 60))
(update :vel-y -)))) ; This negates the y velocity
; You're mutating atoms here, but that's very poor practice.
; This function should be pure and return the new ball
; I also got rid of the draw call here, since this function has nothing to do with drawing
; I moved the drawing to animate!.
(defn advance-ball [ball]
(let [{:keys [pos-x pos-y vel-x vel-y radius]} ball
; You had this appearing after the bounds check.
; I'd think that you'd want to move, then check the bounds.
moved-ball (-> ball
(update :pos-x + vel-x)
(update :pos-y + vel-y))]
(out-of-boundaries moved-ball)))
; For convenience. Not really necessary, but it helps things thread nicer using ->.
(defn advance-balls [balls]
(mapv advance-ball balls))
(defn animate
[]
(swap! balls-atom
(fn [balls]
(doto (advance-balls balls)
(draw-balls)))))
(animate)
我会将所有球作为一个集合保存在一个原子中。每个球都可以表示为 defrecord
,但这里我们只将它们保留为地图。让我们定义两个球:
(def balls (atom [{:pos-x 300
:pos-y 300
:radius 20
:vel-x 1
:vel-y 1}
{:pos-x 500
:pos-y 200
:radius 20
:vel-x -1
:vel-y 1}]))
我会定义一个可以绘制单个球的函数:
(defn draw-ball [ball]
(let [{:keys [pos-x pos-y radius]} ball]
(set! (.-fillStyle c) "black")
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(.fill c)))
既然如此,让我们定义一个函数来清除 canvas:
(defn clear-canvas []
(.clearRect c 0 0 (.-width canvas) (.-height canvas)))
现在,让我们定义一个可以更新单个球的函数:
(defn update-ball [ball]
(let [{:keys [pos-x pos-y radius vel-x vel-y]} ball
bounce (fn [pos vel upper-bound]
(if (< radius pos (- upper-bound radius))
vel
(- vel)))
vel-x (bounce pos-x vel-x (.-width canvas))
vel-y (bounce pos-y vel-y (.-height canvas))]
{:pos-x (+ pos-x vel-x)
:pos-y (+ pos-y vel-y)
:radius radius
:vel-x vel-x
:vel-y vel-y}))
通过以上,我们可以定义我们的动画循环
(defn animate []
(.requestAnimationFrame js/window animate)
(let [updated-balls (swap! balls #(map update-ball %))]
(clear-canvas)
(run! draw-ball updated-balls)))
关键思想是:
- 每个实体(球)都表示为一张地图
- 我们定义了单独的函数来绘制和更新球
- 一切都存储在一个原子中
一些优点:
- 绘制函数很容易单独测试
- 更新功能很容易在 REPL 上测试(可以说,它可以通过传入 canvas 宽度和高度来进一步清理,因此它是纯净的)
- 由于所有状态都在一个原子中,因此很容易
reset!
它具有一些新的所需状态(可能添加新的球,或者只是为了调试目的)
我正在学习 Clojurescript,同时将其与 Javascript 进行比较并重写一些脚本。
在 Javascript 中,我创建了一个 canvas,里面有一个球,当它到达 canvas 的边界时,它会反弹回来。我在 Clojurescript 中做了同样的事情,它有效,但我需要在函数外创建 atoms
,这样我就可以跟踪状态。如果我想创造更多的球,我将需要复制那些原子。到那时,代码将变得丑陋。我应该如何更改代码才能创建多个球并且每个球都有自己的状态?
这里是 Javascript 代码:
// Circle object
function Circle(pos_x, pos_y, radius, vel_x, vel_y){
// Starting variables
this.radius = radius;
this.pos_x = pos_x;
this.pos_y = pos_y;
this.vel_x = vel_x;
this.vel_y = vel_y;
// Draw circle on the canvas
this.draw = function(){
c.beginPath();
c.arc(this.pos_x, this.pos_y, this.radius, 0, Math.PI * 2, false);
c.strokeStyle = this.color;
c.lineWidth = 5;
c.fillStyle = this.color_fill;
c.fill();
c.stroke();
};
// Update the circle variables each time it is called
this.update = function(){
// Check if it goes out of the width
if(this.pos_x + this.radius > canvas.width || this.pos_x - this.radius < 0){
// Invert velocity = invert direction
this.vel_x = -this.vel_x;
}
// Check if it goies out of the height
if(this.pos_y + this.radius > canvas.height || this.pos_y - this.radius < 0){
this.vel_y = -this.vel_y;
}
// Apply velocity
this.pos_x += this.vel_x;
this.pos_y += this.vel_y;
// Draw circle
this.draw();
};
};
// Create a single circle
let one_circle = new Circle(300, 300, 20, 1, 1);
function animate(){
requestAnimationFrame(animate);
// Clear canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// Update all the circles
one_circle.update();
}
animate();
这是 Clojurescript 代码:
(def ball-x (atom 300))
(def ball-y (atom 300))
(def ball-vel-x (atom 1))
(def ball-vel-y (atom 1))
(defn ball
[pos-x pos-y radius]
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c))
(defn update-ball
[]
(if (or (> (+ @ball-x radius) (.-width canvas)) (< (- @ball-x radius) 0))
(reset! ball-vel-x (- @ball-vel-x)))
(if (or (> (+ @ball-y radius) (.-height canvas)) (< (- @ball-y radius) 60))
(reset! ball-vel-y (- @ball-vel-y)))
(reset! ball-x (+ @ball-x @ball-vel-x))
(reset! ball-y (+ @ball-y @ball-vel-y))
(ball @ball-x @ball-y 20))
(defn animate
[]
(.requestAnimationFrame js/window animate)
(update-ball))
(animate)
编辑:我尝试了一种新的方法来解决这个问题,但这不起作用。球已创建,但它不会移动。
(defrecord Ball [pos-x pos-y radius vel-x vel-y])
(defn create-ball
[ball]
(.beginPath c)
(.arc c (:pos-x ball) (:pos-y ball) (:radius ball) 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c))
(def balls (atom {}))
(reset! balls (Ball. 301 300 20 1 1))
(defn calculate-movement
[ball]
(let [pos-x (:pos-x ball)
pos-y (:pos-y ball)
radius (:radius ball)
vel-x (:vel-x ball)
vel-y (:vel-y ball)
new-ball (atom {:pos-x pos-x :pos-y pos-y :radius radius :vel-x vel-x :vel-y vel-y})]
; Check if out of boundaries - width
(if (or (> (+ pos-x radius) (.-width canvas)) (< (- pos-x radius) 0))
(swap! new-ball assoc :vel-x (- vel-x)))
; Check if out of boundaries - height
(if (or (> (+ pos-y radius) (.-height canvas)) (< (- pos-y radius) 60))
(swap! new-ball assoc :vel-y (- vel-y)))
; Change `pos-x` and `pos-y`
(swap! new-ball assoc :pos-x (+ pos-x (@new-ball :vel-x)))
(swap! new-ball assoc :pos-x (+ pos-y (@new-ball :vel-y)))
(create-ball @new-ball)
(println @new-ball)
@new-ball))
(defn animate
[]
(.requestAnimationFrame js/window animate)
(reset! balls (calculate-movement @balls)))
(animate)
在@Carcigenicate 的帮助下,这是一个有效的脚本。
;;; Interact with canvas
(def canvas (.getElementById js/document "my-canvas"))
(def c (.getContext canvas "2d"))
;;; Set width and Height
(set! (.-width canvas) (.-innerWidth js/window))
(set! (.-height canvas) (.-innerHeight js/window))
(defrecord Ball [pos-x pos-y radius vel-x vel-y])
; Making the atom hold a list to hold multiple balls
(def balls-atom (atom []))
; You should prefer "->Ball" over the "Ball." constructor. The java interop form "Ball." has a few drawbacks
; And I'm adding to the balls vector. What you had before didn't make sense.
; You put an empty map in the atom, then immedietly overwrote it with a single ball
(doseq [ball (range 10)]
(swap! balls-atom conj (->Ball (+ 300 ball) (+ 100 ball) 20 (+ ball 1) (+ ball 1))))
; You called this create-ball, but it doesn't create anything. It draws a ball.
(defn draw-ball [ball]
; Deconstructing here for clarity
(let [{:keys [pos-x pos-y radius]} ball]
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(set! (.-lineWidth c) 5)
(set! (.-fillStyle c) "red")
(.fill c)
(.stroke c)))
(defn draw-balls [balls]
(doseq [ball balls]
(draw-ball ball)))
(defn out-of-boundaries
[ball]
"Check if ball is out of boundaries.
If it is, returns a new ball with inversed velocities."
(let [{:keys [pos-x pos-y vel-x vel-y radius]} ball]
;; This part was a huge mess. The calls to swap weren't even in the "if".
;; I'm using cond-> here. If the condition is true, it threads the ball. It works the same as ->, just conditionally.
(cond-> ball
(or (> (+ pos-x radius) (.-width canvas)) (< (- pos-x radius) 0))
(update :vel-x -) ; This negates the x velocity
(or (> (+ pos-y radius) (.-height canvas)) (< (- pos-y radius) 60))
(update :vel-y -)))) ; This negates the y velocity
; You're mutating atoms here, but that's very poor practice.
; This function should be pure and return the new ball
; I also got rid of the draw call here, since this function has nothing to do with drawing
; I moved the drawing to animate!.
(defn advance-ball [ball]
(let [{:keys [pos-x pos-y vel-x vel-y radius]} ball
; You had this appearing after the bounds check.
; I'd think that you'd want to move, then check the bounds.
moved-ball (-> ball
(update :pos-x + vel-x)
(update :pos-y + vel-y))]
(out-of-boundaries moved-ball)))
; For convenience. Not really necessary, but it helps things thread nicer using ->.
(defn advance-balls [balls]
(mapv advance-ball balls))
(defn animate
[]
(swap! balls-atom
(fn [balls]
(doto (advance-balls balls)
(draw-balls)))))
(animate)
我会将所有球作为一个集合保存在一个原子中。每个球都可以表示为 defrecord
,但这里我们只将它们保留为地图。让我们定义两个球:
(def balls (atom [{:pos-x 300
:pos-y 300
:radius 20
:vel-x 1
:vel-y 1}
{:pos-x 500
:pos-y 200
:radius 20
:vel-x -1
:vel-y 1}]))
我会定义一个可以绘制单个球的函数:
(defn draw-ball [ball]
(let [{:keys [pos-x pos-y radius]} ball]
(set! (.-fillStyle c) "black")
(.beginPath c)
(.arc c pos-x pos-y radius 0 (* 2 Math/PI))
(.fill c)))
既然如此,让我们定义一个函数来清除 canvas:
(defn clear-canvas []
(.clearRect c 0 0 (.-width canvas) (.-height canvas)))
现在,让我们定义一个可以更新单个球的函数:
(defn update-ball [ball]
(let [{:keys [pos-x pos-y radius vel-x vel-y]} ball
bounce (fn [pos vel upper-bound]
(if (< radius pos (- upper-bound radius))
vel
(- vel)))
vel-x (bounce pos-x vel-x (.-width canvas))
vel-y (bounce pos-y vel-y (.-height canvas))]
{:pos-x (+ pos-x vel-x)
:pos-y (+ pos-y vel-y)
:radius radius
:vel-x vel-x
:vel-y vel-y}))
通过以上,我们可以定义我们的动画循环
(defn animate []
(.requestAnimationFrame js/window animate)
(let [updated-balls (swap! balls #(map update-ball %))]
(clear-canvas)
(run! draw-ball updated-balls)))
关键思想是:
- 每个实体(球)都表示为一张地图
- 我们定义了单独的函数来绘制和更新球
- 一切都存储在一个原子中
一些优点:
- 绘制函数很容易单独测试
- 更新功能很容易在 REPL 上测试(可以说,它可以通过传入 canvas 宽度和高度来进一步清理,因此它是纯净的)
- 由于所有状态都在一个原子中,因此很容易
reset!
它具有一些新的所需状态(可能添加新的球,或者只是为了调试目的)