Fork me on GitHub
#membrane
<
2022-04-15
>
zimablue11:04:42

So skimming the code and doing tests, Origin is the origin of the OUTERMOST element, and bounds is the bottom right bounds relative to the origin, which is talking about the outermost element as I just said. That explains these results: (defn shift [el] (ui/translate 100 100 el)) (defn rshift [el] (ui/translate -100 -100 el)) (defn origin-bounds [el] [(ui/origin el) (ui/bounds el)]) (def l (ui/label "hello")) (def l2 (shift l)) (def l3 (shift l2)) (def l+-1 (-> l shift rshift)) (def l-+1 (-> l rshift shift)) (pprint (mapv origin-bounds [l l2 l3 l+-1 l-+1])) [[[0 0] [31.723999999999997 13.238000000000001]] [[100 100] [31.723999999999997 13.238000000000001]] [[100 100] [131.724 113.238]] [[-100 -100] [131.724 113.238]] [[100 100] [-68.27600000000001 -86.762]]] (mapv #(ui/within-bounds? l3 %1) [[10 10] [110 110] [210 210]]) [nil [10 10] [110 110]] So one can obtain "absolute collision bottom right" by adding origin to bounds, but there's no obvious way to obtain "absolute collision top left", and this is reflected in within-bounds giving a funky result for double translations. Maybe double translation is a strange pattern but it feels like a natural thing to do, one solution to this narrow problem might be that outer translations traverse their drawables and offset the inner translations instead of wrapping them in an outer Translate but personally I don't think that's nice as you're making something that's supposed to be "data like" magically disappear itself. To me it feels like there should be a dual function to the way that "bounds" operates that takes mins and not maxes, then we have a symmetric language that can describe both upper left and bottom right, having just "origin" and "bottom right relative to origin" with no "top left relative to origin" makes the code I'm trying to write for constraints in my layout difficult I think.

zimablue11:04:54

thinking further, if all layout negotiation happens in the same "origin" context, between parents and direct children then none of this is as relevant, as there's unlikely to be nested translations, but if your layout looks less like a tree and more like a list, you want to have robust non-super-local ways to do layout by negotiating spacing, if that makes sense

zimablue11:04:09

hope this is vaguely relevant and not just the ramblings of a madman

phronmophobic18:04:12

It's hard to tell what results you got that didn't match expectations. There's probably an improvement to be made here, but not totally sure what it is yet. I don't think the issue is double translation. I think it's negative translation. Are there any examples that don't match expectations that don't use negative translations? I don't think double translations is that weird. It's almost certainly going to happen in any reasonably sized app. The most common example would probably be translating a child component that uses a translation in its definition. As mentioned in the slack discussion linked below, I think IBounds is overloaded and should be split into multiple interfaces at some point. > To me it feels like there should be a dual function Yea, it is kinda wonky that bounds kinda implies that the element never extends the left or above the origin. You may also find these discussions relevant: • https://clojurians.slack.com/archives/CVB8K7V50/p1647996723834979https://github.com/phronmophobic/membrane/discussions/43

phronmophobic18:04:55

Now that I've had a chance to use membrane.ui quite a bit, I'm now convinced that the right foundation to build on is 2D/3D geometry (which seems obvious in retrospect). I haven't yet had the time to figure out the best way to do that. The good news is that clojure's data modeling should make it possible to build an improved ui model, extend support to the current ui model, and not break anyone's code.

phronmophobic18:04:54

I wonder if at least some of the issues would be fixed by calculating origin similar to bounds (ie. groups that calculate their bounds based on their children also calculate their origin based on their children).

chromalchemy18:04:32

I remember the http://famo.us css ui framework that was heralded in 2012 (but then turned out to be mostly vaporware?) had some blog posts asserting that 3D was the proper foundation for even a 2D ui framework. https://news.ycombinator.com/item?id=19238340

phronmophobic18:04:15

Are 2D and 3D mutually exclusive?

phronmophobic18:04:45

my intuition is that a good model wouldn't make you decide

chromalchemy19:04:10

I guess 2D abstractions were derived from a 3D environment/context?

chromalchemy19:04:52

Their claim to fame was doing some Flash-like fx and transitions, in early html5 days.

phronmophobic19:04:56

my comment is that it seems likely that you could define everything in terms of operations that are polymorphic with respect to the coordinate space (ie. provide definitions for translate, scale, intersect, etc that work for 2d or 3d elements)

zimablue19:04:43

I think the within-bounds call for the rectangle is odd

zimablue19:04:05

because it's making the translation count as part of the bounds for an object

zimablue19:04:16

I don't think negative translations are that odd

zimablue19:04:00

re geometry I agree but I would as I studied physics, when using your framework I wrote convenience method that used 2-vectors and some simple 2-vector math, since the invariants of 2d geometry let you do more things "for free"

zimablue19:04:26

eg. you can apply arbitrary scaling, rotation not just translation if everything is couched in terms of vectors

zimablue19:04:43

if x and y are random parameters not treated as a vector then you have to pack and unpack them all the time to achieve that

zimablue19:04:47

it's not so much that the results of my work were unexpected, as I said they are explained by my interpretation of them within what I intepreted that they meant in the code, the concrete point is that "find me the upper left edge for collision purposes" is currently not an idea inherent to the framework

zimablue19:04:09

since as the within-bounds call shows, origin is NOT that, although it's sort of conflated to be

zimablue19:04:48

bounds+origin == absolute bottom right for collision purposes, origin != absolute top left for collision purposes, ??? = absolute top left for collision purposes and it feels to me like ??? should be a concept in the framework and it would be calculated a bit like how bounds is in this part: (extend-protocol IBounds #?(:cljs cljs.core/PersistentVector :clj clojure.lang.PersistentVector) Recursively calling itself and instead of taking reduce... max ... taking reduce ... min ...

zimablue19:04:43

Another way I was thinking to say, is that only being able to calculate the bottom right and not the top left is OK if you "calculate" your UI top to bottom and left to right, but if you calculate right to left, or lay out some arbitrary elements and then try to fit things around them/draw lines connecting them, you're a bit stuck as you can't universally find the left/top edges only the right/bottom

phronmophobic20:04:13

> I don't think negative translations are that odd totally agree. Just pointing out that I think that's a use case where the current model falls down, but stacked translations shouldn't be an issue.

zimablue20:04:23

the within-bounds call that I think is odd doesn't fall down on the negatively translated element, just the double translated one

phronmophobic20:04:59

> (origin-bounds l3)
[[100 100] [130.79296875 114.0]]
> (ui/within-bounds? l3 [10 10])
nil
> (ui/within-bounds? l3 [110 100])
[10 0]
> (ui/within-bounds? l3 [210 210])
[110 110]
That seems right, right?

phronmophobic20:04:34

within-bounds? returns the position in l3 's local coordinates

phronmophobic20:04:28

I do think that more consistently defining origin as the upper left of an element would be an improvement though, https://clojurians.slack.com/archives/CVB8K7V50/p1650048234221789?thread_ts=1650021822.257629&amp;cid=CVB8K7V50

phronmophobic20:04:21

I'll have to think about it a bit, but that's likely an easy improvement.

zimablue20:04:10

so 10 10 is nil because it doesn't accept negatives?

phronmophobic20:04:43

no, [10 10] is nil because it's not within [[100 100] [130.79296875 114.0]]

zimablue21:04:50

but neither is [210 210]

zimablue21:04:49

ah, maybe it's within some notional bounds, but the actual rectangle is only like 10 by 30, the point I'm making with that test is that it's counting the vec-translate towards the bounds of the object

zimablue21:04:44

sorry *the ui/Translate

zimablue21:04:00

any concept of bounds for a 10 by 30 rectangle which includes points [100 100] apart is different that I'd have expected

phronmophobic21:04:05

I think that part is correct

zimablue21:04:28

yeah that bug you posted seems very relevant

phronmophobic21:04:51

I think that bug should be fixed in the latest version on github

zimablue21:04:54

because to get bottom right from a translated element you add (bounds origin) but to get bottom right from a padded element you just do (bounds padded-element)

phronmophobic21:04:29

Yes, this should be fixed on master, https://github.com/phronmophobic/membrane/blob/master/src/membrane/ui.cljc#L810 I haven't pushed a new version to clojars, but I can do that soon if that helps.

phronmophobic21:04:00

I'll also look into trying "consistently defining origin as the upper left of an element" issue soon as well.

zimablue21:04:44

sorry if I didn't express this clearly, now that you summarize it I think you understood what I was reaching for already

gratitude 1
phronmophobic21:04:33

no problem. I'm pretty sure I'm not explaining myself well and I could still be wrong. It's been really helpful to get multiple eyes on the project. It's already surfaced several relevant issues.

phronmophobic21:04:16

I've known that negative origins has been an issue, but I think your breakdown has helped clarify a path forward!

zimablue21:04:18

I read the issue and pondered, I still think top-left is a separate notion from origin, and should just be added. Your example proves it:

[(ui/translate 5 5
     (ui/rectangle 5 5)]
if origin is zero and bounds are ten, no-one can ask this group the real question (in your own coordinate system, what is your top left point?) I think this group has: origin 0 0 bounds 10 10 top-left 5 5 and we can't get rid of any of them

phronmophobic21:04:47

there are 3 elements here: 1.

[(ui/translate 5 5
     (ui/rectangle 5 5)]
2.
(ui/translate 5 5
     (ui/rectangle 5 5)
3.
(ui/rectangle 5 5)

phronmophobic21:04:22

I'm not sure why the top left for the group would be 5,5?

phronmophobic21:04:05

(ui/on :mouse-down f
       [(ui/translate 5 5
                      (ui/rectangle 5 5))])
;; vs
[(ui/translate 5 5
               (ui/on :mouse-down f
                      (ui/rectangle 5 5)))]

zimablue21:04:22

because the top left and the bounds are relative to the origin, I guess

phronmophobic21:04:42

Are you suggesting top left is used for hit detection or something to do with drawing bounds?

zimablue21:04:53

or just packing boxes right-to-left

zimablue21:04:25

the reason that bottom-right works most of the time is that almost all UIs just pack everything left-to-right top-to-bottom and then delegate subcontainers to do the same thing again recursively

phronmophobic21:04:15

can you give an example of how top left would be used?

zimablue21:04:16

yeah hit detection, but the maths of hit detection also = the maths of element collision detection

zimablue21:04:00

can you give an example of how top left would be used?

zimablue21:04:10

the app I'm trying to write connects boxes with arrows

zimablue21:04:49

first I generate the layout of the boxes by a spacing algorithm, and I generate the membrane components, then knowing which boxes are connected to which I generate edges which connect the right of one box to the left of another

zimablue21:04:31

so at that point I have a load of membrane components (translated Rectangles/Labels) and I need to find the left and right edges of them

zimablue21:04:25

if there's no reliable "what's your left bound", I have to invent one basically, and if I ever change the component type (padding/translate/offset/rect) then I'll have to write a new calculation to find the left hand edge, since atm it's all different and somewhat inconsistent

zimablue21:04:35

basically because top-left doesn't exist, I'm forced to invent it

zimablue21:04:09

because I'm not laying out left to right top to bottom, if you're doing that you never notice that it's difficult to ask that question because you're only ever asking "what's your right-hand edge"

phronmophobic21:04:20

I think the trick here is that elements don't actually have hitboxes, they just have bounds and origins.

;; this element does not have a hit box
(ui/on :mouse-down f
       [(ui/translate 5 5
                      (ui/rectangle 5 5))])
;; this element does
[(ui/translate 5 5
               (ui/on :mouse-down f
                      (ui/rectangle 5 5)))]

phronmophobic21:04:21

I think I have an example that might help. one sec

zimablue21:04:31

(def could-be-any-element [(ui/translate 5 5
     (ui/rectangle 5 5)])
Imagine you are passed this element and have to write code that draws an arrow to the vertical-centre horizontal-left of the box

zimablue21:04:17

and try to do it with only origin or bounds, you can't do it. the midpoint between origin and bounds isn't the midpoint of the rectangle, because origin != top left

zimablue21:04:31

*origin and bounds

phronmophobic21:04:47

That's a good example. Let me explain how I would do it. I'm not saying my approach is the best way, but maybe it will be useful as a way to figure what kind of improvements we should look at.

phronmophobic21:04:53

Writing up an example... one sec.

👍 1
phronmophobic22:04:53

So there's multiple ways to do this. One way is to just use defrecord to create a Box element that implements origin and bounds that match what you expect. I think there's an implicit assumption that the hitbox for an element that contains a Rectangle should match the hitbox for the (single?) Rectangle, but I'm not sure that's a generalizable expectation. The only way I can think to generalize that is to use ui/on to wrap the rectangle explicitly. I'm not sure (although I could be convinced) that top left is generic property, but I think you could still solve this problem using generic data manipulation code. For tree walking, I tend to lean on zippers and some helpers I've written, but it might look funny without the helpers or if zippers are unfamiliar. Here's how that might look:

(require '[clojure.zip :as z])
(def element [(ui/translate 5 5
                            (ui/rectangle 5 5))])

(defn is-diagram-box? [elem]
  (instance? membrane.ui.Rectangle elem))

(defn elem-zip [elem]
  (z/zipper (constantly true)
            ui/children
            (fn [elem _] elem)
            elem))

(defn top-left [elem]
  (let [zelem
        ;; find the box in the element tree
        (loop [zip (elem-zip elem)]
          (if (z/end? zip)
            nil
            (if (is-diagram-box? (z/node zip))
              zip
              (recur (z/next zip)))))
        ;; translate its origin to global coordinates
        pt (loop [zip zelem
                  [x y] [0 0]]
             (if zip
               (recur (z/up zip)
                      (let [[ox oy] (ui/origin (z/node zip))]
                        [(+ x ox) (+ y oy)]))
               [x y]))]
    pt))

> (top-left element)
;; [5 5]
> (top-left [element])
;; [5 5]
> (top-left (ui/translate 5 5 [element]))
;; [10 10]
> (top-left (ui/translate -5 -5 (ui/translate 5 5 [element])))
;; [5 5]

zimablue22:04:37

reading your code but just to say that I'm not doing hitboxes atm they're not in my brain so maybe a distraction or maybe a useful metaphor not sure

phronmophobic22:04:05

yea, maybe hitboxes isn't the right word

zimablue22:04:21

re generalizability, one could imagine that if the element was text, a box, a circle, pointing an arrow to the left hand side of the visible region would still be useful, so it's reasonable to try and write a component-agnostic function

zimablue22:04:37

you are assuming that it's a polygon-or-text-contiguous-thing I agree

zimablue22:04:53

or one wrapped in padding and-or offsets

phronmophobic22:04:11

so the top-left function in my example uses is-diagram-box?, but it could be re-written to top-left corner of any element that matches a provided predicate

zimablue22:04:26

you code melted my brain, I'm not familiar with zippers, but I think I get the gist, find the box and reverse traverse through origins

phronmophobic22:04:17

yea, zippers are really great for tree walking. z/next just does a depth first traversal

zimablue22:04:12

now what if you have an arbitrary selection of elements and want to draw a vertical line aligned with the leftmost element?

zimablue22:04:29

your code avoids implementing a bounds-like function because my example uses a single element

zimablue22:04:08

but I think the space of UIs-in-canvases contains multiple usecases for (same as bounds but the other side)

zimablue22:04:53

sorry to throw another example after you wrote clever code for the first one, but I think the "real" function is just a dual of bounds, like I was saying with instead of reduce ... max ... for groups, a reduce ... min ... for groups

zimablue22:04:47

the function you've written is like if one wrote a bounds function but had a guarantee that it's only a single type of element that "qualifies", so could just do a single traversal and no reduce

zimablue22:04:30

not even a single type, a single (or the first-found) "instance" of that single type

phronmophobic22:04:08

It's pretty similar

(defn origin->global-coords [zelem]
  (loop [zip zelem
         [x y] [0 0]]
    (if zip
      (recur (z/up zip)
             (let [[ox oy] (ui/origin (z/node zip))]
               [(+ x ox) (+ y oy)]))
      [x y])))

(defn zip-seq [elem]
  (->> (elem-zip elem)
       (iterate z/next)
       (take-while #(not (z/end? %)))))

(defn left-most-point [elem pred]
  (->> (zip-seq elem)
       (filter #(is-diagram-box? (z/node %)))
       (reduce (fn [x zelem]
                 (let [[ox oy] (origin->global-coords zelem)]
                   (if (< ox x)
                     ox
                     x)))
               Long/MAX_VALUE)))

> (left-most-point element is-diagram-box?) ;; 5
> (left-most-point [element] is-diagram-box?) ;; 5
> (left-most-point [element
	            (ui/translate -5 -5 element)]
                   is-diagram-box?) ;; 0

> (left-most-point [(ui/translate -5 -5 element)
                    element]
                   is-diagram-box?) ;; 0

zimablue22:04:03

you've passed your coding interview by the way congratulations

🎉 1
zimablue22:04:09

wish I could jam out functions this fast

phronmophobic22:04:18

It's both the blessing and the curse of clojure. On the one hand, all the neat data manipulation tools clojure offers can be used directly for this domain and I don't have to build anything specific into membrane, but on the other hand, it does mean there's no baked-in default approach.

zimablue22:04:50

let me get some sleep and I'll try and grok those functions in a day or two, thanks for your time

👍 1
zimablue22:04:47

a thought is that with padding, unlike translate, the user has semantically (imo) stated that they want the element to always have space on the left, this would ignore the left padding intention

zimablue22:04:51

one could code that out by dispatching on the element types but once you're doing lots of dispatching on element types it's sort of evidence for a missing protocol

zimablue23:04:13

another example is a circle, it seems very natural that the origin of a circle be his centre, which would be different from his bounding-rect-top-left

zimablue23:04:32

if the origin of an Arc is the top-left, then the origin changes depending upon how much of the arc you draw

phronmophobic23:04:29

yea, I agree that bounds and origin are both overloaded. I think this is where borrowing concepts from geometry and graphic design would help.

chromalchemy18:04:50

I think bounds, from a graphic design perspective is under-represented in software. Usually you get a line that is like a hard boundary or, to which a stroke (and maybe fill) can be applied. (traditional vector graphics). But in practice I end up embellishing these boundaries, with things like drop shadows, glows, and interior gradients. Usually they are applied uniformly across a shape or line. But I would like to apply them selectively or partially. Usually I would need to “paint” that as needed; or fiddle with the elements on both sides, back and forth to get it to look right. It is quite imperative and tedious. In the case of the spread of a drop shadow, or a thick border, the effective boundary is extended. It becomes a grey zone of in or out. This is probably more related to the notion of a contrast zone (or gradient), involving more than one discrete shape. I think there could be a thing where you draw an unclosed line, that is not to be drawn itself, but informs the bounds of a contrast. Example: everything immediately to one side is a little darker, other side a little lighter, while the elements bordering it have their own intrinsic definitions, that feed in to the rendering. It could even oscillate back and forth. Like what if you could declare you want a shape’s edges to locally “pop”, relative to what’s around it. Sorry if this is tangent to your more technical conversation, but it might be food for though about the scope how boundaries could be defined. Design is all about contrasts (or lack thereof), both local and global. Currently there is little way to declare them in a functional way. And making them more dynamic and tweakable could be a huge boon.