thi-ng / umbrella

⛱ Broadly scoped ecosystem & mono-repository of 199 TypeScript projects (and ~180 examples) for general purpose, functional, data driven development
https://thi.ng
Apache License 2.0
3.38k stars 150 forks source link

[hiccup-svg] hiccup-svg's attributes don't seem to handle rstream #459

Closed arnaudchenyensu closed 5 months ago

arnaudchenyensu commented 6 months ago

Hi!

I'm currently learning/experimenting with your different packages and I noticed that hiccup-svg's attributes don't seem to handle rstream, e.g:

// taken from one of your example
import { defAtom, defHistory } from "@thi.ng/atom";
import { equivArrayLike } from "@thi.ng/equiv";
import { circle, svg } from "@thi.ng/hiccup-svg";
import { $compile, $list } from "@thi.ng/rdom";
import { fromAtom, reactive } from "@thi.ng/rstream";
import { indexed, repeatedly } from "@thi.ng/transducers";
import { random2 } from "@thi.ng/vectors";

const WIDTH = 600;
const NUM = 4;
const R = 20;

// define atom of NUM random points
const atom = defAtom([...repeatedly(() => random2([], R, WIDTH - R), NUM)]);
const db = defHistory(atom)

// reactive subscription for atom changes
const $db = fromAtom(db);

// ID of currently dragged point
const clicked = reactive(-1);

$compile([
  'div',
  {},
  [
    'div',
    {},
    ['button', { onclick: () => db.undo() }, 'Undo'],
    ['button', { onclick: () => db.redo() }, 'Redo'],
  ],
  svg(
    {
      width: WIDTH,
      height: WIDTH,
      viewBox: `0 0 ${WIDTH} ${WIDTH}`,
      onmousemove: (e: MouseEvent) =>
        clicked.deref() !== -1 && db.resetIn([clicked.deref()!], [e.clientX, e.clientY]),
      onmouseup: () => clicked.next(-1),
      ondblclick: (e: MouseEvent) =>
        db.swap((pts) => [...pts, [e.clientX, e.clientY]]),
    },
    $list(
      $db.map((pts) => [...indexed(0, pts)]),
      "g",
      {
        fill: 'white',
        stroke: 'black'
      },
      ([i, p]) =>
        circle(p, R, {
          onmousedown: (e: MouseEvent) => {
            clicked.next(i);
            db.resetIn([i], [e.clientX, e.clientY]);
          },
          fill: clicked.map(idx => idx === i ? 'grey' : 'white')
        }),
      equivArrayLike
    ),
  )
]).mount(document.getElementById("app")!);

Is it expected? How should I do if I want the fill color of a circle to change when the clicked stream is updated?

Thank you in advance, Arnaud.

arnaudchenyensu commented 6 months ago

I tried to do it this way (by using sync) but without much success (I have duplicated circles on click):

import { defAtom, defHistory } from "@thi.ng/atom";
import { circle, svg } from "@thi.ng/hiccup-svg";
import { $compile, $klist } from "@thi.ng/rdom";
import { fromAtom, reactive, sync } from "@thi.ng/rstream";
import { repeatedly } from "@thi.ng/transducers";
import { random2, type Vec } from "@thi.ng/vectors";

const WIDTH = 600;
const NUM = 4;
const R = 20;

// define atom of NUM random points
const atom = defAtom([...repeatedly(() => random2([], R, WIDTH - R), NUM)]);
const db = defHistory(atom)

// reactive subscription for atom changes
const $db = fromAtom(db);

// ID of currently dragged point
const clicked = reactive(-1);

$compile([
  'div',
  {},
  [
    'div',
    {},
    ['button', { onclick: () => db.undo() }, 'Undo'],
    ['button', { onclick: () => db.redo() }, 'Redo'],
  ],
  svg(
    {
      width: WIDTH,
      height: WIDTH,
      viewBox: `0 0 ${WIDTH} ${WIDTH}`,
      onmousemove: (e: MouseEvent) =>
        clicked.deref() !== -1 && db.resetIn([clicked.deref()!], [e.clientX, e.clientY]),
      onmouseup: () => clicked.next(-1),
      ondblclick: (e: MouseEvent) =>
        db.swap((pts) => [...pts, [e.clientX, e.clientY]]),
    },
    $klist(
      sync({ src: {
        clicked,
        pts: $db,
      }}).map((src) => src.pts.map((pt, i): [number, number, Vec, string] => (
        [src.clicked, i, pt, src.clicked === i ? 'grey' : 'white']
      ))),
      "g",
      {
        fill: 'white',
        stroke: 'black'
      },
      ([_idx, i, p, fill]) =>
        circle(p, R, {
          id: i,
          onmousedown: (e: MouseEvent) => {
            clicked.next(i);
            db.resetIn([i], [e.clientX, e.clientY]);
          },
          fill
        }),
      ([_idx, i, p, fill]) => `${i}-${p}-${fill}`
    ),
  )
]).mount(document.getElementById("app")!);
postspectacular commented 6 months ago

Hi @arnaudchenyensu - so sorry, completely missed this issue... I will take a look & report back asap!

arnaudchenyensu commented 5 months ago

Hi @postspectacular , did you get any chance to look into it? :)

postspectacular commented 5 months ago

Hi @arnaudchenyensu - yes I did and been meaning to respond here with update (also just pushed a feature branch with somewhat of a compromise I worked on a while ago, but still not sure about it), but haven't gotten around to write full explanation until now...

The short of it is that because the way the current (hiccup-svg) API is designed, it's not easily possible (or was originally intended) to provide the same kind of reactive attributes as with the functions provided by the hiccup-html package. A simple example to illustrate:

import { circle } from "@thi.ng/hiccup-svg";
import { lch } from "@thi.ng/color";

circle([100,200], 300, { fill: lch(0.5,1,0), scale: 2, translate: [1000, 2000] })
// [
//   'circle',
//   {
//     fill: '#ff007c',
//     cx: '100',
//     cy: '200',
//     r: '300',
//     transform: 'translate(1000 2000) scale(2)'
//   }
// ]

Here, the following happened:

As you can see, even in this primitive example there's no direct 1:1 mapping between function args and resulting SVG attributes (that was kind of the whole point of this package) and so far I can't quite see how to overcome this to provide support for reactive values being used as inputs, and even questioning if we should.

If you want reactive SVG elements in a rdom component tree you can already do so and have options:

1) Use plain hiccup arrays instead of hiccup-svg to define your elements/attribs 2) Use the hiccup-svg (or even thi.ng/geom) to define your shapes/geometries and then replace elements where needed using rdom's control wrappers, e.g. $replace().

These are also combinable, as you can see in the rdom-svg-nodes. I've also just added a brand new rdom-reactive-svg example to show another combined approach:

Live demo

Sourcecode: https://github.com/thi-ng/umbrella/blob/4a9bba9c26117468057e14a4854a47a8c4b21a9e/examples/rdom-reactive-svg/src/index.ts#L1-L40


Some more thoughts about the aforementioned feature branch: feature/hiccup-svg-deref-attribs has experimental support for [IDeref-wrapped]() values, but the fundamental problem still persists that there's an unresolved discrepancy between the current hiccup-svg API and a resulting data structure which is more amenable to reactive changes/updates. The only way I can see this changing is via massive breaking changes, new dependencies, or best, a new rdom support package (since the original purpose of hiccup-svg for static SVG generation).

All I can say is that I'll keep thinking about it, but also keen to hear your/other's thoughts...

import { reactive } from "@thi.ng/rstream";

const pos = reactive([100, 200]);
const scale = reactive(2);

// same circle as other example, only partially with reactive attribs
circle(pos, 300, { fill: lch(0.5,1,0), scale, translate: [1000, 2000] })

// same result as before:
// [
//   'circle',
//   {
//     fill: '#ff007c',
//     cx: '100',
//     cy: '200',
//     r: '300',
//     transform: 'translate(1000 2000) scale(2)'
//   }
// ]

Even though this might feel like a step forward, I'm questioning the need for this extra complexity since this support for deref()-able (reactive) values doesn't make the result reactive (yet?).

postspectacular commented 5 months ago

@arnaudchenyensu To summarize some more from my above comment: The two fundamental issues and reasons for "incompatibility" here are that hiccup-svg:

Both of these aspects are by design, but they also limit that package's use to only support reactivity on a per-shape, but not per-attribute basis. As I mentioned above, for a version support the latter, I think the best way forward would be a new support package for rdom (e.g. rdom-svg). This could even mirror mostly the same API as hiccup-svg, but would internally use a similar approach as I've shown in the new rdom-reactive-svg example, i.e. attaching attribute specific subscriptions/formatters for all shape params which need it...

I can definitely see value in that too, but would still like to hold off working on this for a while since I'm currently busy with adding more support for ES async iterables and consolidating functionality overlap between this native language feature and rstream constructs. A lot of this will impact rdom too and my plan is to try to decouple rdom from rstream entirely (also leading to smaller bundle sizes). I've written more about this on my Mastodon...

https://mastodon.thi.ng/@toxi/112338024068213892

arnaudchenyensu commented 5 months ago

Hi @postspectacular , thank you for your answers and examples; now it makes more sense and I'm perfectly fine using plain hiccup.

In my opinion (as a new user of your packages), it can be confusing knowing which element will be automatically updated and which will not. For example, just like you mentioned in your rdom-reactive-svg example, it's fine to use svg and group from hiccup-svg but we need to use plain hiccup for circles.

I would prefer to have consistency that nicely tie everything together. In this regard, having a package rdom-svg would make it more obvious that the preferred way to use svg with rstream is rdom-svg.

I don't think that's worth it to break hiccup-svg API for rstream. In my mind, hiccup-svg could be considered "completed" just like you did with hdom for example.

It's just my personal opinion obviously 😄

On another subject, your work is very inspiring and I always come back to it when I have a tricky problem to solve, to see "How @postspectacular would I have solved it" 😆

postspectacular commented 5 months ago

I fully agree @arnaudchenyensu & I'm really sorry for the confusion! I know there're a lot (too many?) options, but they mostly stem from different use cases (and/or historic reasons). Feedback like this is really super helpful, though, and I think confusion can to some extend be addressed/lowered with more docs (e.g. here in this case pointing out clearly which element functions will translate inputs into which kind of attributes).

In the long run though, an rdom-svg package is gonna come (and hopefully in the not too distant future)! 👍