Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
  • Jun 23 01:05
    raquo locked #98
  • Jun 23 01:05
    raquo unlocked #98
  • Jun 23 01:02
    raquo locked #98
  • Jun 23 01:01
    raquo closed #98
  • Jun 23 01:01
    raquo commented #98
  • Jun 22 15:10
    yatesco opened #98
  • Jun 18 20:37
    raquo commented #97
  • Jun 18 20:37

    raquo on master

    Doc typo Changed variable name (compare)

  • Jun 18 20:37
    raquo closed #97
  • Jun 18 14:39
    Quafadas review_requested #97
  • Jun 18 14:39
    Quafadas opened #97
  • Jun 09 05:14

    raquo on gh-pages

    Deploy website Deploy website … (compare)

  • Jun 03 06:32
    raquo commented #93
  • Jun 02 16:25
    raquo commented #93
  • Jun 02 15:18
    rparree commented #93
  • Jun 02 15:18
    rparree commented #93
  • May 15 23:25

    raquo on gh-pages

    Deploy website Deploy website … (compare)

  • May 15 23:21

    raquo on mdoc-issue

    (compare)

  • May 15 23:21

    raquo on develop

    (compare)

  • May 15 23:20

    raquo on scala3

    (compare)

Kit Langton
@kitlangton
Making some progress. (try the right and left arrow keys)
I'm curious. Is there a trusted way of tracking the old values of a signal of DOM elements? I'd like to fade out the previous values rather than straight up disappearing them :)
Nikita Gazarov
@raquo

Looks nice! I get a feeling that there's more code running than is needed though, with the page taking up 20% of a core with no animations running. Still runs pretty smooth though

Is there a trusted way of tracking the old values of a signal of DOM elements? I'd like to fade out the previous values rather than straight up disappearing them :)

Well... Airstream has fold operators that let you accumulate state that might help... Although what would be more useful I think is Laminar's children.command API (see docs) – it might be a better fit for your use case than children <--. If you track which DOM elements need to be added / removed yourself, you can issue such commands to Laminar using children.command <--.

Kit Langton
@kitlangton

@raquo It definitely is :) I've got to figure out a way to toggle off the stream once the animation has finished, but then toggle it back on if the value signal emits again.

The very simple implementation of spring is:

  def spring[A]($value: Signal[A], stiffness: Double = 170, damping: Double = 26, delayMs: Int = 0)(implicit animatable: Animatable[A]): Signal[A] = {
    val delayedValue = $value.composeChanges(_.delay(delayMs.toInt))

    Time.timeSignal
      .combineWith(delayedValue) // <== Here
      .fold[Map[String, Spring]]({
        case (t, start) =>
          animatable
            .toAnimations(start)
            .map {
              case (key, value) =>
                key -> Spring(value, t)
                  .copy(stiffness = stiffness, damping = damping)
            }
      }) {
        case (animations, (t, target)) =>
          val next = animatable.toAnimations(target)
          animations.map {
            case (key, animation) =>
              (key, animation.setTarget(next(key)).tick(t))
          }
      }
      .map(_.map(v => v._1 -> v._2.value))
      .map(animatable.fromAnimations)
  }

Is there a straightforward/performant method of ignoring the timeSignal once the animation has ended? (I have an isDone method on the Spring values in the map and can forall them to figure out when the animation has completed)

Kit Langton
@kitlangton
And thank you for the children.command suggestion! I will try that out :)
Nikita Gazarov
@raquo

Just to get this out of the way, you're doing complicated stuff so the solution isn't a one-liner. Normally Laminar stuff is much easier.

So you have a Time.timeSignal that emits on every animation frame. It's shared, so that's some good savings already. But you also have N animatable elements and N observables that emit as often as Time.timeSignal ALL THE TIME, regardless of whether the animation is actually going on. This is what's causing most of performance issues.

You need a different, more low level approach than just mapping over Time.timeSignal and putting the resulting observable into Laminar. You need to use Ownership, probably even Dynamic Ownership (see Airstream docs for both) to manage the lifecycle of your observables. Currently and normally you just let Laminar take care of the observable's lifecycle – when your element is mounted, all its subscriptions like attr <-- observable and children <-- observable are activated, and those observables start to listen to their parents and emit values. And they stop when the element is unmounted. But that's too broad for your case, what you want is a different lifecycle for your N subscriptions, you want to start them only when the animation starts, and stop them when the animation stops (and also stop them when the element is unmounted). That way only the observables relevant to the animations that are currently in progress will emit events.

This is a bit more complicated than I can design and code off the top of my head. The basic idea is that instead of creating a Signal[String] that you can plug into someAttr <--, you want to create a Modifier, probably a Binder, that will do something similar, but it would manage the lifecycle of the animation observable manually. it would need to take several parameters including someAttr as well as some data structure that would tell it when the animation starts and ends, and maybe that same Signal[String] that the modifier would start and stop when the animation starts or stops.

Although, I guess you can avoid manually managing ownership... Keep in mind that a shared Time.timeSignal strictly speaking does not have to remain a part of your design in that exact shape. Another way to accomplish what you need – again, in very high level terms – is as follows: Say you have Signal[Value] and you want to animate the transition from v1 to v2 by animating an attribute from a1 to a2. So you do something like myAttr <-- valueSignal.map(createAnimationStream(myAttr)).flatten. And createAnimationStream reads the current state of myAttr from the element, and creates a stream of values to animate it smoothly to some new value based on what valueSignal emitted. You'll need to read about flattening in airstream docs to make sense of that. The interesting part is what happens inside createAnimationStream. When it's triggered with a new value from valueSignal, it should create a new EventBus, and it should manually call requestAnimationFrame as many times as needed (but not more, do your math to stop in time), and every time the callback is called, emit an event with the next attribute value to the event bus, and return this stream. So then when you stop calling requestAnimationFrame, the stream for this attribute will stop updating. And when your valueSignal emits a new value, this stream will be discarded and a new one will be created and that one will also only run until the animation is done. This assumes that calling requestAnimationFrame is cheap (I don't actually know). If that assumption is wrong, you could retain the shared Time.timeSignal and instead implement this idea by using addSource / removeSource on EventBus to manage the lifecycle of the stream - add it to start animation, remove when animation is done. You should probably also cache your computation of animation coordinates so that they don't get re-computed on every animation frame.

Alright, well. Lots of text... I think my proposition in the last paragraph is doable. But you're throwing yourself into the deep end. This is hard stuff. You need to understand lazyness and lifecycle and ownership pretty well to make sense of it.
@kitlangton ^
If Airstream had stream completion feature this would be slightly easier. This is probably the first good use case for it that I ran into.
Kit Langton
@kitlangton
Wow! Thank you, @raquo. This gives me a lot to go on. I’ve already played around with flatMap in an implementation of keyframe animations (press the ‘2’ key on https://laminate.surge.sh/ to see a silly demo) and had some doubts around my approach. But I think EventBus may provide the key (or at least a piece of the key!).
Hopefully I succeed in getting something nice out of this, but either way, it’s a real fun problem to think about. Also, if anyone here has any favorite animation libraries, I’m looking for anything to steal good ideas from ;)
Iurii Malchenko
@yurique
Just thinking aloud: would making start/stop built-in into the binders help in cases like this?
Iurii Malchenko
@yurique

@raquo
does this look correct/make sense?
https://gist.github.com/yurique/72877e9aa7248a0ce54438af7a4f20db
(usage example: https://gist.github.com/yurique/96bfd3aeec101e3de6805a631ec03947)

I had a use case for it, but as it often happens I found another way to do what I wanted. But I had this coded already.

Kenner Stross
@kennerstross
@kitlangton - If interested in animation libraries to inspire you, I highly recommend GSAP: https://greensock.com/
Nikita Gazarov
@raquo

@yurique Hm, not sure of what the intent of that is:

bindSwitching($observables) --> { v => println(s"switching: $v") }

Is it intended to be the equivalent of

$observables.flatten --> { v => println(s"switching: $v") }

and if not, what are the desired differences in behaviour?

Granted, FlattenStrategy[Signal, Observable, _] does not currently exist in Airstream, but if that's what's desired you could implement it fairly easy I think.


Anyway, to answer your question, you have too many nested DynamicSubscription-s for no reason – you're not using their activation / deactivation feature, and you're not passing them to laminar for laminar to do that for you. Your code should be, roughly:

    override def bind(element: El): DynamicSubscription = {
      ReactiveElement.bindSubscription(element) { ctx =>
        $observable.foreach(innerObservable => innerObservable.foreach(onNext)(ctx.owner))(ctx.owner)
      }
    }

$observable.foreach(innerObservable => innerObservable.foreach(onNext)(ctx.owner))(ctx.owner) will be run on activation, and ctx.owner will kill all of its subscriptions – both the outer and inner one – on deactivation. I'm not sure what the exact requirements are but I guess this is what you're after.

would making start/stop built-in into the binders help in cases like this

It's already there, pretty much. If you create a custom DynamicSubscription, you just add any start code to its activate callback, and any stop code to the cleanup callback of the Subscription that activate returns.

Nikita Gazarov
@raquo
Or do you mean like create a version of ReactiveElement.bindCallback that also accepts a deactivate callback? I guess that would simplify the syntax a bit.
Iurii Malchenko
@yurique

@raquo thanks!

this:

 $observable.foreach(innerObservable => innerObservable.foreach(onNext)(ctx.owner))(ctx.owner)

is what I was looking for and couldn’t get my head around :)

And you’re right, it’s equivalent to having

val signalOfStreams: Signal[EventStream[T]] = ???
div(
signalOfStreams.flatten —> …
)

But the strategy doesn’t exist indeed, I assumed there was a reason for that.

as for the start/stop — I mean it in the context of the animations and stopping things when the animation is over, etc
Kit Langton
@kitlangton

I rewrote spring using an EventBus. It feels smoother than the previous version (or at least just as smooth—as the main problem w/ the last version was that it never stopped calculating! :P)

Animatable[A] now translates its A back and forth from a mutable.IndexedSeq[Double], which should be a bit faster.

Perhaps it's a bit less "elegant" (not that the first version was particularly nice), but it's worth it if it fixes the performance problems :) Which it seems to!

  def spring[A]($value: Signal[A], stiffness: Double = 170, damping: Double = 26, delayMs: Int = 0)(implicit animatable: Animatable[A]): Signal[A] = {
    val delayedValue = $value.composeChanges(_.delay(delayMs.toInt))
    val runner       = new SpringRunner[A] {}

    delayedValue.flatMap(runner.animateTo(_, stiffness = stiffness, damping = damping))
  }

  class SpringRunner[A](implicit animatable: Animatable[A]) {
    var values: mutable.IndexedSeq[Spring] = mutable.IndexedSeq.empty[Spring]
    var timeBus                            = new EventBus[Double]
    var animating                          = false

    def time(): Int =
      dom.window.requestAnimationFrame(stepTime)

    def stepTime(t: Double): Unit = {
      timeBus.writer.onNext(t)
      if (animating) {
        time()
      }
    }

    def animateTo(value: A, stiffness: Double = 170, damping: Double = 26): Signal[A] = {
      if (values.isEmpty) {
        values = animatable.toAnimationsSeq(value).map(d => Spring.fromValue(d, 0))
      } else {
        val nextValues = animatable.toAnimationsSeq(value)
        var i          = -1
        values.mapInPlace { spring =>
          i += 1
          spring.setTarget(nextValues(i))
        }
      }

      val signal = timeBus.events
        .map { t =>
          values.mapInPlace(_.tick(t))
          if (values.forall(_.isDone)) {
            animating = false
          }
          animatable.fromAnimationsSeq(values)
        }
        .startWith(animatable.fromAnimationsSeq(values))

      if (!animating) {
        animating = true
        time()
      }

      signal
    }
  }
Nikita Gazarov
@raquo
Yeah something like that. Did you push that to surge.sh already?
BTW One thing that I forgot to mention is that you can of course implement all this without streams. If you want to squeeze max performance you can always drop to simply calling methods like DomApi.removeHtmlAttribute in response to requestAnimationFrame bypassing event buses and observables entirely.
Kit Langton
@kitlangton
Ah, no, not pushed yet :)
And that’s true :P But I really like how it’s composable w/ the rest of Laminar/Airstream. And it seems to be working fast enough, at least for my purposes.
Kit Langton
@kitlangton
Republished! The home page is a blog skeleton I'm working on, but there are links to the spring/keyframes examples.

I love how Waypoint lets me use the browser's prev/next page shortcuts to trigger animations ^_^.

I've gotta say: Laminar is my favorite way of coding for the web by many miles. It's so ridiculously good! I really hope I can convince my next employer to use this.

Nikita Gazarov
@raquo
Glad to hear it :)
Demo cpu usage is barely registering now, +1
Iurii Malchenko
@yurique

hey @raquo
so I wanted to get rid of my modify extension and replace it with the built-in amend and it looks like it’s broken, as both overloads match:

div().amend(cls := “test”)
[error] both method amend in trait ReactiveElement of type ((mods: com.raquo.domtypes.generic.Modifier[_1.type]*)_1.type) forSome { val _1: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Div] }
[error] and  method amend in trait ReactiveElement of type ((mod: com.raquo.domtypes.generic.Modifier[_1.type])_1.type) forSome { val _1: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Div] }
  @inline def amend(mod: Modifier[this.type]): this.type = {
    mod(this)
    this
  }

  def amend(mods: Modifier[this.type]*): this.type = {
    mods.foreach(mod => mod(this))
    this
  }

So if we want to keep the @inline single param overload the second probably one needs to be like amend(first: Modifier[this.type], second: Modifier[this.type], rest: Modifier[this.type]*), or we can just drop the @inline overload.

Kit Langton
@kitlangton
Ah, yeah. I also had trouble getting amend to work :)
Nikita Gazarov
@raquo
Hm. So this only happens if you inline div() for some reason. If you extract div() into a val, it compiles just fine. WTF...
Nikita Gazarov
@raquo
Thanks for bringing this up!
Nikita Gazarov
@raquo
I wonder if this fork of scala-js-env-jsdom-nodejs that supports require() can be used to run tests with JS dependencies without scalajs-bundler.... https://github.com/exoego/scala-js-env-jsdom-nodejs
Kit Langton
@kitlangton
I got element transitions working pretty well :D
(just type, and it'll toggle a view on or off for the typed key)
The signature is pretty much split except with an additional Status signal (Inserting | Removing | Active)
 def splitTransition[A, Key, Output]($as: Signal[Seq[A]], key: A => Key)
                                    (project: (Key, A, Signal[A], Signal[TransitionStatus]) => Output): Signal[Seq[Output]] = ???
`
Kit Langton
@kitlangton
And then that signal can easily be mapped and passed as an argument to spring:
        onMountBind { ctx =>
          maxHeight <-- spring($status.map {
            case Active => ctx.thisNode.ref.scrollHeight.toDouble
            case _      => 0.0
          }).px
        },
Iurii Malchenko
@yurique

I wonder if this fork of scala-js-env-jsdom-nodejs that supports require() can be used to run tests with JS dependencies without scalajs-bundler.... https://github.com/exoego/scala-js-env-jsdom-nodejs

this one looks promising

Nikita Gazarov
@raquo
@kitlangton Note that if all you need from ctx is thisNode, you can use inContext { thisNode => instead of onMountBind { ctx =>; the latter is a heavier construct
przemekd
@przemekd
hey, any idea how to implement something like this -> https://javascript.info/mouse-drag-and-drop using Laminar?
przemekd
@przemekd
how could I remount element to HTML body tag and change its position on mouse move events?
przemekd
@przemekd
and how can I dynamically add and remove event listeners from objects?
Nikita Gazarov
@raquo

Hey, good question, a few messages to follow:

and how can I dynamically add and remove event listeners from objects?

Two options:
1) the normal way – add the event listener like so: onMouseMove --> callback, and it will be removed automatically when you unmount the element on which you set this listener
2) the manual way – call ReactiveElement.addEventListener and then ReactiveElement.removeEventListener with the exact same param. You will need to create an EventPropBinder but that's straightforward, like new EventPropBinder(onMouseMove, callback, useCapture = false)

Nikita Gazarov
@raquo

how could I remount element to HTML body tag

First, don't mount the element to <body>, create an app container <div> inside the body, and render your whole Laminar app into it, like shown in examples. Then you can have something like

val movingBallBus = new EventBus[Option[HtmlElement]]

div(
  id := "app-container",
  // ... the rest of your app here ...
  child.maybe <-- movingBallBus.events
)

Then you need to say movingBallBus.writer.onNext(Some(yourElement)) when you want yourElement to appear in the app-container div, and movingBallBus.writer.onNext(None) when you want it to disappear.

Note that yourElement can only appear in one place at the same time. You can't render the same element in two places at the same time, it's impossible in JS DOM, and if you try that, you might experience unexpected behavior.

change its position on mouse move events?

Something like:

div(
  inContext { thisNode =>
    onMouseMove --> { ev => thisNode.ref.style.top = ev.clientY }
  }
)
Nikita Gazarov
@raquo

how to implement something like this -> https://javascript.info/mouse-drag-and-drop using Laminar?

This particular example can be translated to Laminar fairly directly, you just need to call Laminar's wrappers ReactiveElement.addEventListener and ReactiveElement.removeEventListener instead of using plain JS methods, and use element.ref to get the real DOM node from a Laminar element. Start with something like:

div(
  id="ball",
  onMouseDown --> { ev => ... }
)

@przemekd

przemekd
@przemekd
@raquo thanks for your help, that's a neat idea to have EventBus with [Option[HtmlElement]. I would have the div appearing in the top-parent. But after one would stop dragging the <div> over a .droppable div, how could I place it under a new parent? That's what I'm missing.