A drawing's origin is the middle of what it draws
A shape keyed from the bottom left to the top centre with a 360° turn on the way
left the stage completely in the middle of the spin and came back. The keys were
right and every frame between them was wrong, which is the signature of a wrong
pivot and a full turn: 0° and 360° are the only two frames where a wrong pivot
cannot be seen at all.
`paint/new-shape` stored a stroke EXACTLY AS DRAWN, in the containing symbol's
coordinates, and wrote no `pos`. So a drawing's origin was the SYMBOL's origin —
on the stage, its top-left corner. `node/local!` turns and scales about the
node's own origin and nothing else, deliberately, since `[:xform :anchor]` was
deleted in 925c12f. A node whose origin is nowhere near its content therefore
turns about nowhere near its content: the reported shape orbited at a radius of
126 px on a 320x200 stage.
It could not be seen while a drag was the only way to turn something, because
`gesture/about` solves for the `pos` that holds the chosen pivot still and
`turn` wrote it alongside the rotation — exactly right on the frame of the drag.
But that solution is `p' = c + R(θ)(p − c)`, an ARC, and `pos` interpolates along
the CHORD. Right on a drag, right on a key, wrong on every frame between two.
So `paint/centred` splits a stroke into a ring about its own middle and the `pos`
that puts it back, and `new-shape` is the one place every drawing is born — the
pen, the brush, and each piece the eraser leaves. The pivot rule is unchanged,
the middle of what the node draws; for a drawing that point is now its ORIGIN, so
`gesture/at-origin?` holds, `turn` writes `rot` alone, `scale` writes `scale`
alone, and a keyed turn is right on every frame. `pos` goes back to being the
motion path it reads as. Hand-authored scenes were always written this way:
`demo/scene.edn`'s card is `[-44 -30 44 -30 44 30 -44 30]` with its place in
`pos`.
NOT the universal rule, and `a-face-part-scales-about-its-own-middle` is why. A
measured part's points and position are dense tier-2 geometry in the footage's
space and cannot be re-originated, so its pivot is not its origin and `about` is
the only thing that will hold it; the same is true of an instance, whose origin
IS its symbol's coordinate system. Both still drag correctly about their middle,
both are inexact if that drag is keyed, and for both a pivot that has to persist
or be keyed is a peg — which is a node, so its pivot is its own origin, so it
collapses again one level up.
`cut/erase` took EVERY leftover piece back through `world⁻¹`, the cut shape's own
coordinates, and handed the offcuts to `new-shape`, which gives them a fresh
identity transform. Those two spaces coincide only while a shape has `pos [0 0]`,
which was every shape, so erasing anything that had been moved already scattered
its offcuts, silently. The kept piece comes back through `world⁻¹` and the new
ones through `parent⁻¹`, the space a node's `pos` lives in.
And `::adjust-last` re-traces the same stroke from stage pixels, so it goes
through `paint/place-points` rather than writing symbol-space points into a node
that now has a position of its own.
No schema change: the same fields, better values. An existing document keeps
evaluating exactly as it does now.
`a-keyed-turn-holds-its-pivot-between-its-keys` checks all 31 frames of the
tween. Checking the keys is what let this through. 604 CLJS tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e021a13eb
commit
e7f5f82845
11 changed files with 344 additions and 104 deletions
|
|
@ -12,6 +12,19 @@
|
|||
(let [b (:buf (r/fill-poly-buf! (r/make 60 60) ring (quot (count ring) 2) 1))]
|
||||
(fn [x y] (aget b (+ x (* y 60))))))
|
||||
|
||||
(defn- placed
|
||||
"Node `id`'s drawing on frame 0 back in the space it was drawn in: its own
|
||||
points plus its `pos`.
|
||||
|
||||
A shape's points are about its OWN ORIGIN and its `pos` says where that origin
|
||||
is, so the ring to rasterise in stage pixels is the sum — `paint/centred`. The
|
||||
cut is still checked in the pixels it was made in; it is read back out through
|
||||
the transform the shape now has, rather than by assuming it has none."
|
||||
[nodes id]
|
||||
(let [[cx cy] (channel/value-at (get-in nodes [id :channels [:xform :pos]]) 0 nil)]
|
||||
(into [] (map-indexed (fn [i v] (+ v (if (even? i) cx cy))))
|
||||
(channel/value-at (get-in nodes [id :channels paint/geometry]) 0 nil))))
|
||||
|
||||
(deftest a-cut-off-the-side-keeps-the-rest-with-the-cut-edge-as-its-points
|
||||
(let [[left & more] (cut/cut square [[[40 0 60 0 60 60 40 60]]])]
|
||||
(is (empty? more))
|
||||
|
|
@ -46,10 +59,13 @@
|
|||
(let [clip (-> (clip/blank) (paint/new-shape :main :s 0 square 3))
|
||||
out (cut/erase clip nil :main 0 [[:s]] [[[25 0 30 0 30 60 25 60]]] [:t :u])
|
||||
nodes (get-in out [:symbols :main :nodes])
|
||||
pts #(channel/value-at (get-in nodes [% :channels paint/geometry]) 0 nil)]
|
||||
pts #(placed nodes %)]
|
||||
(is (= #{:s :t} (set (keys nodes))))
|
||||
(is (= 1 ((ink (pts :s)) 40 30)))
|
||||
(is (= 1 ((ink (pts :t)) 15 30)))
|
||||
(is (= 1 ((ink (pts :t)) 15 30))
|
||||
"the offcut is a new shape of its own, and is where it was cut from — its
|
||||
points come back through the space its `pos` lives in, not through the
|
||||
transform of the shape it was cut out of")
|
||||
(is (= 3 (channel/value-at (get-in nodes [:t :channels [:style :color]]) 0 nil)))
|
||||
(is (= {} (get-in (cut/erase clip nil :main 0 [[:s]] [[[0 0 60 0 60 60 0 60]]] [])
|
||||
[:symbols :main :nodes]))
|
||||
|
|
@ -60,4 +76,5 @@
|
|||
out (cut/erase clip nil :main 5 [[:s]] [[[40 0 60 0 60 60 40 60]]] [])
|
||||
ks (get-in out [:symbols :main :nodes :s :channels paint/geometry :keys])]
|
||||
(is (= #{0 5 10} (set (keys ks))))
|
||||
(is (= square (ks 0) (ks 10)))))
|
||||
(is (= (first (paint/centred square)) (ks 0) (ks 10))
|
||||
"the keys either side keep the shape's own points, untouched")))
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
[c st sid id frame v0]
|
||||
(gesture/pivot v0 ((pick/bounds-of c st sid (get-in c [:symbols sid :nodes id])) frame)))
|
||||
|
||||
(defn- at [m p] (let [out (js/Float64Array. 2)]
|
||||
(vec (array-seq (node/apply-pt! out 0 m (first p) (second p))))))
|
||||
|
||||
(defn- dragged [c path f vs-fn]
|
||||
(let [{:keys [sid id frame] :as pl} (nest/placement c nil :main path 16)
|
||||
v0 (gesture/values (get-in c [:symbols sid :nodes id]) frame nil)]
|
||||
|
|
@ -71,20 +74,56 @@
|
|||
(get-in out [:symbols :main :nodes :shape :channels [:xform :pos] :interp])))))
|
||||
|
||||
(deftest turning-keeps-the-pivot-where-it-is
|
||||
;; The shape's geometry is [0 0 10 0 5 10], so the middle of what it draws is
|
||||
;; (5, 5) in its own coordinates. NOTHING STORES THAT: the turn solves for the
|
||||
;; `pos` that holds it still, which is the whole of why the rest goes round it.
|
||||
;; The shape was drawn at [0 0 10 0 5 10], so the middle of what it draws is
|
||||
;; (5, 5) of the space it was drawn in — and `paint/centred` made that the
|
||||
;; node's OWN ORIGIN, which is (0, 0) in its own coordinates. So the pivot is
|
||||
;; the origin, and the turn is one channel: there is no `pos` to solve for,
|
||||
;; because `node/local!` already turns about exactly this point.
|
||||
(let [c (two-down)
|
||||
path [u v :shape]
|
||||
{:keys [world]} (nest/placement c nil :main path 16)
|
||||
pivot #(let [out (js/Float64Array. 2)]
|
||||
(vec (array-seq (node/apply-pt! out 0 (:world (nest/placement % nil :main path 16)) 5 5))))
|
||||
(vec (array-seq (node/apply-pt! out 0 (:world (nest/placement % nil :main path 16)) 0 0))))
|
||||
turned (dragged c path 16 #(gesture/turn %2 %3 0.7))]
|
||||
(is (some? world))
|
||||
(is (near? (pivot c) (pivot turned)) "the middle of the drawing stays put")
|
||||
(is (not (near? (drawn c path) (drawn turned path))) "and the rest goes round it")
|
||||
(is (contains? (get-in turned [:symbols :box :nodes :shape :channels]) [:xform :pos])
|
||||
"a turn writes pos as well as rot — that is what makes it happen about a point")))
|
||||
(is (= #{[:xform :rot]} (set (keys (gesture/turn (gesture/values
|
||||
(get-in c [:symbols :box :nodes :shape]) 4 nil)
|
||||
(pivot-of c nil :box :shape 4
|
||||
(gesture/values
|
||||
(get-in c [:symbols :box :nodes :shape]) 4 nil))
|
||||
0.7))))
|
||||
"a turn about the node's own origin writes rot ALONE — a pos written
|
||||
beside it would be solved for this frame and interpolated along the
|
||||
chord of an arc between keys, which is a keyed spin leaving the stage")))
|
||||
|
||||
(deftest a-keyed-turn-holds-its-pivot-between-its-keys
|
||||
;; THE BUG, as it was reported: a shape keyed from the bottom left to the top
|
||||
;; centre with a 360° turn on the way flew right off the stage in the middle of
|
||||
;; the spin and came back. 0° and 360° are the two frames where a wrong pivot
|
||||
;; cannot be seen, so the keys looked right and every frame between them did
|
||||
;; not. Here the whole tween is checked, which is the only way this is caught.
|
||||
(let [drawn-at [100 80 140 80 140 110 100 110]
|
||||
c (-> (clip/blank)
|
||||
(paint/new-shape :main :shape 0 drawn-at :brow)
|
||||
;; Keyed by hand, as the inspector keys: bottom left to top
|
||||
;; centre, one full turn on the way.
|
||||
(assoc-in [:symbols :main :nodes :shape :channels [:xform :pos]]
|
||||
(ch/keyed {0 [60 150] 30 [160 40]} :linear))
|
||||
(assoc-in [:symbols :main :nodes :shape :channels [:xform :rot]]
|
||||
(ch/keyed {0 0 30 (* 2 js/Math.PI)} :linear)))
|
||||
middle (fn [f]
|
||||
(let [{:keys [world]} (nest/placement c nil :main [:shape] f)
|
||||
[x0 y0 x1 y1] ((pick/bounds-of c nil :main
|
||||
(get-in c [:symbols :main :nodes :shape])) f)]
|
||||
(at world [(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)])))]
|
||||
(doseq [f (range 0 31)]
|
||||
(let [[x y] (middle f)
|
||||
t (/ f 30)]
|
||||
(is (near? [x y] [(+ 60 (* t 100)) (- 150 (* t 110))])
|
||||
(str "frame " f ": the middle of the drawing is at " (pr-str [x y])
|
||||
", not on the straight line from (60, 150) to (160, 40)"))))))
|
||||
|
||||
(deftest scaling-takes-the-grabbed-point-to-the-pointer
|
||||
(let [c (two-down)
|
||||
|
|
@ -92,13 +131,15 @@
|
|||
{:keys [world]} (nest/placement c nil :main path 16)
|
||||
out (js/Float64Array. 2)
|
||||
at #(vec (array-seq (node/apply-pt! out 0 %1 %2 %3)))
|
||||
p0 (at world 10 0)
|
||||
;; (5, -5) of the shape's own coordinates is the point drawn at (10, 0):
|
||||
;; the ring is centred, so what was drawn is offset by its own middle.
|
||||
p0 (at world 5 -5)
|
||||
p1 [(+ (first p0) 3) (- (second p0) 5)]
|
||||
grown (dragged c path 16 #(gesture/scale %1 %2 %3 p0 p1 false))
|
||||
w2 (:world (nest/placement grown nil :main path 16))]
|
||||
(is (near? p1 (at w2 10 0))
|
||||
(is (near? p1 (at w2 5 -5))
|
||||
"the corner grabbed is under the pointer, through a turned, unevenly scaled parent")
|
||||
(is (near? (at world 5 5) (at w2 5 5)) "about the middle of the drawing")))
|
||||
(is (near? (at world 0 0) (at w2 0 0)) "about the middle of the drawing, which is its origin")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; which way a corner drag goes
|
||||
|
|
@ -117,14 +158,12 @@
|
|||
;; None of it is stored any more, so none of it can be absent or stale. These
|
||||
;; tests used to need a freeze pass to have written the pivots they check.
|
||||
|
||||
(defn- at [m p] (let [out (js/Float64Array. 2)]
|
||||
(vec (array-seq (node/apply-pt! out 0 m (first p) (second p))))))
|
||||
|
||||
(defn- handles
|
||||
"Where the stage would draw this node's box and pivot: its own bounds through
|
||||
its `:world`, and the MIDDLE OF THOSE BOUNDS, which is what `ui/stage`'s
|
||||
`handles` does. One `bounds-of` feeding both is the point — the cross cannot
|
||||
drift off the box, because it is the box's own middle."
|
||||
drift off the box, because it is the box's own middle. On a drawing it is also
|
||||
the node's own origin, because that is where `paint/centred` put it."
|
||||
[c st open path f]
|
||||
(let [{:keys [sid id world frame]} (nest/placement c st open path f)
|
||||
n (get-in c [:symbols sid :nodes id])
|
||||
|
|
@ -340,5 +379,7 @@
|
|||
(deftest an-instances-box-is-what-its-symbol-draws
|
||||
(let [c (two-down)
|
||||
{:keys [frame]} (nest/placement c nil :main [u v] 16)]
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil :mid (get-in c [:symbols :mid :nodes v])) frame)))
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil :box (get-in c [:symbols :box :nodes :shape])) 4)))))
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil :mid (get-in c [:symbols :mid :nodes v])) frame))
|
||||
"an instance's box is what its symbol draws, in the symbol's space")
|
||||
(is (= [-5 -5 5 5] ((pick/bounds-of c nil :box (get-in c [:symbols :box :nodes :shape])) 4))
|
||||
"and the shape's own box is about its own origin, which is its middle")))
|
||||
|
|
|
|||
|
|
@ -85,8 +85,11 @@
|
|||
((clip/resolver % :main nil pal/index-of nil) 16))))))
|
||||
{:keys [frame matrix time]} (nest/inside c nil :main [u v :shape] 16)
|
||||
out (js/Float64Array. 2)
|
||||
;; THE SHAPE'S OWN COORDINATES, which is the centred ring `new-shape`
|
||||
;; stored and not the points handed to it: a drawing's origin is the
|
||||
;; middle of what it draws, and `matrix` is the map out of that space.
|
||||
seen (mapcat (fn [[x y]] (vec (array-seq (node/apply-pt! out 0 matrix x y))))
|
||||
(partition 2 [0 0 10 0 5 10]))
|
||||
(partition 2 (first (paint/centred [0 0 10 0 5 10]))))
|
||||
[x y] (array-seq (node/apply-pt! out 0 (node/invert matrix) 7 3))
|
||||
moved (paint/set-vertex c :box :shape frame 0 [x y])
|
||||
keyed (paint/add-key c :box :shape (:frame (nest/inside c nil :main [u v :shape] 20)))]
|
||||
|
|
|
|||
|
|
@ -12,22 +12,30 @@
|
|||
(get-in clip [:symbols :main :nodes :paint-test :channels paint/geometry]))
|
||||
|
||||
(deftest drawing-keys-hold-and-tween-on-the-symbol-clock
|
||||
(let [a [10 10 30 10 20 30]
|
||||
;; `a` is what was drawn, `ring` is what the node holds: the same drawing about
|
||||
;; its own middle, with the middle in `pos`. Every point below is in the node's
|
||||
;; own coordinates, which is the space `set-vertex` writes in.
|
||||
(let [a [10 10 30 10 20 30]
|
||||
ring (first (paint/centred a))
|
||||
c0 (paint/new-shape demo/clip :main :paint-test 3 a :brow)
|
||||
c1 (paint/add-key c0 :main :paint-test 9)
|
||||
c2 (paint/set-vertex c1 :main :paint-test 9 0 [22 10])
|
||||
c2 (paint/set-vertex c1 :main :paint-test 9 0 [2 -10])
|
||||
c3 (paint/add-key c2 :main :paint-test 15)
|
||||
c4 (paint/set-vertex c3 :main :paint-test 15 0 [34 10])
|
||||
c4 (paint/set-vertex c3 :main :paint-test 15 0 [14 -10])
|
||||
held (geometry c4)
|
||||
mixed-clip (update-in c4 [:symbols :main :nodes :paint-test]
|
||||
node/set-segment-interp paint/geometry 9 :linear)
|
||||
mixed (geometry mixed-clip)]
|
||||
(is (= [3 229] (get-in c2 [:symbols :main :nodes :paint-test :span])))
|
||||
(is (= a (channel/value-at held 8 nil)))
|
||||
(is (= 10 (first (channel/value-at held 8 nil))))
|
||||
(is (= 22 (first (channel/value-at held 9 nil))))
|
||||
(is (= 10 (first (channel/value-at mixed 6 nil))) "the first gap cuts")
|
||||
(is (= 28 (first (channel/value-at mixed 12 nil))) "the second gap tweens")
|
||||
(is (= [-10 -10 10 -10 0 10] ring) "centred on the middle of its own box")
|
||||
(is (= [20 20] (channel/value-at (get-in c0 [:symbols :main :nodes :paint-test
|
||||
:channels [:xform :pos]]) 3 nil))
|
||||
"and placed back where it was drawn")
|
||||
(is (= ring (channel/value-at held 8 nil)))
|
||||
(is (= -10 (first (channel/value-at held 8 nil))))
|
||||
(is (= 2 (first (channel/value-at held 9 nil))))
|
||||
(is (= -10 (first (channel/value-at mixed 6 nil))) "the first gap cuts")
|
||||
(is (= 8 (first (channel/value-at mixed 12 nil))) "the second gap tweens")
|
||||
(is (empty? (channel/problems mixed)))
|
||||
;; The demo's root is exposed on 2s. Paint at frame 3 must still appear at 3.
|
||||
(is (some #(= :paint-test (:node %))
|
||||
|
|
@ -35,12 +43,13 @@
|
|||
(is (= mixed-clip (leaf/clip :c1 (leaf/leaves :c1 mixed-clip))))))
|
||||
|
||||
(deftest a-point-is-added-and-taken-away-on-every-key
|
||||
;; `[0 0 10 0 10 10]` drawn is `[-5 -5 5 -5 5 5]` held, about its own middle.
|
||||
(let [c0 (-> (paint/new-shape demo/clip :main :p 0 [0 0 10 0 10 10] :brow)
|
||||
(paint/add-key :main :p 6)
|
||||
(paint/set-vertex :main :p 6 1 [20 0]))
|
||||
(paint/set-vertex :main :p 6 1 [15 -5]))
|
||||
c1 (paint/insert-vertex c0 :main :p 0 0.5)
|
||||
ks #(get-in % [:symbols :main :nodes :p :channels paint/geometry :keys])]
|
||||
(is (= {0 [0 0 5 0 10 0 10 10] 6 [0 0 10 0 20 0 10 10]} (ks c1))
|
||||
(is (= {0 [-5 -5 0 -5 5 -5 5 5] 6 [-5 -5 5 -5 15 -5 5 5]} (ks c1))
|
||||
"half way along the same edge of each key")
|
||||
(is (= (ks c0) (ks (paint/delete-vertex c1 :main :p 1))))
|
||||
(is (= (ks c0) (ks (paint/delete-vertex c0 :main :p 0))) "a triangle keeps its three")))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue