brightscriptscenegraph

How to add Slide Animation to Roku's Poster


I am creating Roku channel. In my logic, there is an array of media(image&video), and I am displaying them with their timeout seconds. For example, one image is displaying 5 secs, then goes to next one. I have fading animation when going to next image. But I also need to add sliding animation. This is my code:

xml:

<component name="RendererScreen"
           extends="Group">
    <script type="text/brightscript"
            uri="RendererScreen.bs" />
    <children>
        <Poster id="backgroundImg"
                width="1920"
                height="1080"
                uri="pkg:/images/background.png" />
        <Poster id="imgPoster"
                width="1920"
                height="1080"
                translation="[0, 0]"
                opacity="0.0"
                visible="true" />
        <Animation id="fadeInAnimation"
                   repeat="false"
                   control="stop"
                   easeFunction="linear">
            <FloatFieldInterpolator id="fadeInInterpolator"
                                    key="[0.0, 1.0]"
                                    keyValue="[0.0, 1.0]"
                                    fieldToInterp="imgPoster.opacity" />
        </Animation>
        <Animation id="fadeOutAnimation"
                   repeat="false"
                   control="stop"
                   easeFunction="linear">
            <FloatFieldInterpolator id="fadeOutInterpolator"
                                    key="[0.0, 1.0]"
                                    keyValue="[1.0, 0.0]"
                                    fieldToInterp="imgPoster.opacity" />
        </Animation>
        <Animation id="slideInAnimation"
                   repeat="false"
                   control="stop"
                   easeFunction="linear">
            <FloatFieldInterpolator id="slideInInterpolator"
                                    key="[0.0, 1.0]"
                                    keyValue="[0.0, 1.0]"
                                    fieldToInterp="imgPoster.translation" />
        </Animation>
        <Animation id="slideOutAnimation"
                   repeat="false"
                   control="stop"
                   easeFunction="linear">
            <FloatFieldInterpolator id="slideOutInterpolator"
                                    key="[0.0, 1.0]"
                                    keyValue="[0.0, 1.0]"
                                    fieldToInterp="imgPoster.translation" />
        </Animation>
        <Video id="videoPlayer"
               width="1920"
               height="1080"
               translation="[0, 0]"
               visible="false" />
    </children>
</component>

bs file:

sub init()
    m.poster = m.top.findNode("imgPoster")
    m.backgroundImg = m.top.findNode("backgroundImg")
    m.mediaList = []
    m.currentIndex = 0
    m.deviceOrientation = "90"

    fetchPlaylistData()
end sub

sub showMedia()
    if (m.mediaList.count() = 0)
        return
    end if

    mediaItem = m.mediaList[m.currentIndex]
    mediaType = mediaItem.type
    mediaUrl = mediaItem.url
    timeoutSeconds = mediaItem.timeoutSeconds

    m.backgroundImg.visible = false

    animationDuration = 1
    fadeInAnimation = m.top.findNode("fadeInAnimation")
    fadeOutAnimation = m.top.findNode("fadeOutAnimation")
    fadeInAnimation.duration = animationDuration
    fadeOutAnimation.duration = animationDuration

    ' I need to add here if clause(if animation is fade or slide)

    if (mediaType = "image")
        fadeInAnimation.control = "start"
        displayImage(mediaUrl)
    else if (mediaType = "video")
        displayVideo(mediaUrl)
    end if

    startSlideShowTimer(timeoutSeconds)
end sub

sub displayVideo(url as string)
    if (m.videoPlayer = invalid)
        m.videoPlayer = m.top.findNode("videoPlayer")
        m.videoPlayer.observeField("state", "onVideoStateChange")
    end if

    if (m.videoPlayer <> invalid)
        videoContent = createObject("roSGNode", "ContentNode")
        videoContent.url = url
        m.videoPlayer.content = videoContent
        m.videoPlayer.visible = true
        m.videoPlayer.control = "play"
    else
        print "Error: Video node not found."
    end if
end sub

sub onVideoStateChange()
    if (m.videoPlayer.state = "error")
        m.videoPlayer.control = "stop"
        m.videoPlayer.visible = false
        startErrorDelayTimer() 
    end if
end sub

sub displayImage(url as string)
    if (m.poster <> invalid)
        if(m.deviceOrientation = "90")
            m.poster.width = "1080"
            m.poster.height = "1920"
            m.poster.translation = [420, -420]
            m.poster.rotation = -1.570795 ' -> 3.14159/2 = π/2
        else if(m.deviceOrientation = "180")
            m.poster.width = "1920"
            m.poster.height = "1080"
            m.poster.translation = [0, 0]
            m.poster.rotation = 3.14159 ' -> π
        else if(m.deviceOrientation = "270")
            m.poster.width = "1080"
            m.poster.height = "1920"
            m.poster.translation = [420, -420]
            m.poster.rotation = 1.570795 ' -> 3.14159/2 = π/2
        end if

        m.poster.scaleRotateCenter = [m.poster.width / 2, m.poster.height / 2]
        m.poster.uri = url
        m.poster.observeField("loadStatus", "onPosterStateChange")
        m.poster.visible = true
    else
        print "Error: Poster node not found."
    end if
end sub

sub onPosterStateChange()
    if (m.poster.loadStatus = "failed")
        m.poster.visible = false
        startErrorDelayTimer()
    end if
end sub

sub startErrorDelayTimer()
    if (m.errorDelayTimer = invalid)
        m.errorDelayTimer = createObject("roSGNode", "Timer")
        m.errorDelayTimer.observeField("fire", "onErrorDelayComplete")
        m.top.appendChild(m.errorDelayTimer)
    end if

    m.errorDelayTimer.duration = 3
    m.errorDelayTimer.repeat = false
    m.errorDelayTimer.control = "start"
end sub

sub onErrorDelayComplete()    
    m.currentIndex = (m.currentIndex + 1) mod m.mediaList.count()
    showMedia()
end sub

sub startSlideShowTimer(duration as integer)
    if (m.timer = invalid)
        m.timer = createObject("roSGNode", "Timer")
        m.timer.observeField("fire", "onTimerFired")
        m.top.appendChild(m.timer)
    end if

    m.timer.duration = duration
    m.timer.repeat = false
    m.timer.control = "start"
end sub

sub onTimerFired()
    fadeOutAnimation = m.top.findNode("fadeOutAnimation")
    fadeOutAnimation.observeField("completion", "onFadeOutComplete")
    fadeOutAnimation.control = "start"

    startOutTimer(fadeOutAnimation.duration)
end sub

sub startOutTimer(animationDuration as integer)
    if (m.animationTimer = invalid)
        m.animationTimer = createObject("roSGNode", "Timer")
        m.animationTimer.observeField("fire", "onFadeOutComplete")
        m.top.appendChild(m.animationTimer)
    end if

    m.animationTimer.duration = animationDuration
    m.animationTimer.repeat = false
    m.animationTimer.control = "start"
end sub

sub onFadeOutComplete()
    if (m.videoPlayer <> invalid)
        m.videoPlayer.control = "stop"
        m.videoPlayer.visible = false
    end if

    m.currentIndex = (m.currentIndex + 1) mod m.mediaList.count()
    showMedia()
end sub

Solution

  • To handle sliding in and out, you'll need to maintain two separate posters that you swap between. Here's an example showing how to fade between two posters. We leverage the .delay property on animation to keep the current poster visible for the timeoutSeconds in your example.

    <?xml version="1.0" encoding="utf-8"?>
    <component name="MainScene" extends="Scene">
      <script type="text/brightscript" uri="MainScene.brs" />
    
      <children>
        <Poster id="posterPrimary" width="1920" height="1080" />
        <Poster id="posterSecondary" width="1920" height="1080" />
        <!--slide and fade the secondary poster INTO view and the primary poster OUT of view, -->
        <Animation id="animationShowSecondary" repeat="false" control="stop" easeFunction="linear">
          <FloatFieldInterpolator id="fadeInInterpolator1" key="[0.0, 1.0]" keyValue="[0.0, 1.0]" fieldToInterp="posterSecondary.opacity" />
          <FloatFieldInterpolator id="fadeOutInterpolator1" key="[0.0, 1.0]" keyValue="[1.0, 0.0]" fieldToInterp="posterPrimary.opacity" />
          <Vector2DFieldInterpolator id="slideInInterpolator1" key="[0.0, 1.0]" keyValue="[[1920,0], [0,0]]" fieldToInterp="posterSecondary.translation" />
          <Vector2DFieldInterpolator id="slideOutInterpolator1" key="[0.0, 1.0]" keyValue="[[0,0], [-1920,0]]" fieldToInterp="posterPrimary.translation" />
        </Animation>
        <!--slide and fade the primary poster INTO view and the secondary poster OUT of view, -->
        <Animation id="animationShowPrimary" repeat="false" control="stop" easeFunction="linear">
          <FloatFieldInterpolator id="fadeInInterpolator2" key="[0.0, 1.0]" keyValue="[0.0, 1.0]" fieldToInterp="posterPrimary.opacity" />
          <FloatFieldInterpolator id="fadeOutInterpolator2" key="[0.0, 1.0]" keyValue="[1.0, 0.0]" fieldToInterp="posterSecondary.opacity" />
          <Vector2DFieldInterpolator id="slideInInterpolator2" key="[0.0, 1.0]" keyValue="[[1920,0], [0,0]]" fieldToInterp="posterPrimary.translation" />
          <Vector2DFieldInterpolator id="slideOutInterpolator2" key="[0.0, 1.0]" keyValue="[[0,0], [-1920,0]]" fieldToInterp="posterSecondary.translation" />
        </Animation>
      </children>
    </component>
    
    sub init()
      m.posterPrimary = m.top.findNode("posterPrimary")
      m.posterSecondary = m.top.findNode("posterSecondary")
      m.animationShowSecondary = m.top.findNode("animationShowSecondary")
      m.animationShowPrimary = m.top.findNode("animationShowPrimary")
      'anytime the animations change, we want to know about it
      m.animationShowSecondary.observeFieldScoped("state", "onanimationShowSecondaryStateChange")
      m.animationShowPrimary.observeFieldScoped("state", "onanimationShowPrimaryStateChange")
    
      'list of media items. This can be fetched dynamically from the server, and each one specifies how long they are visible
      m.mediaItems = [{
        uri: "pkg:/media/water.jpg",
        timeoutSeconds: 3
      }, {
        uri: "pkg:/media/bunny.jpg",
        timeoutSeconds: 5
      }]
      'show the first poster
      showImageAtIndex(m.posterSecondary, 1)
      showImageAtIndex(m.posterPrimary, 0)
      m.animationShowSecondary.delay = m.mediaItems[m.currentMediaItemIndex].timeoutSeconds
      m.animationShowSecondary.control = "start"
    end sub
    
    function showImageAtIndex(poster, mediaItemIndex as integer)
      if mediaItemIndex > m.mediaItems.count() - 1
        mediaItemIndex = 0
      end if
      mediaItem = m.mediaItems[mediaItemIndex]
      poster.uri = mediaItem.uri
      m.currentMediaItemIndex = mediaItemIndex
    end function
    
    sub onanimationShowSecondaryStateChange()
      if m.animationShowSecondary.state = "stopped"
        showImageAtIndex(m.posterPrimary, m.currentMediaItemIndex + 1)
        m.animationShowPrimary.delay = m.mediaItems[m.currentMediaItemIndex].timeoutSeconds
        m.animationShowPrimary.control = "start"
      end if
    end sub
    
    sub onanimationShowPrimaryStateChange()
      if m.animationShowPrimary.state = "stopped"
        showImageAtIndex(m.posterSecondary, m.currentMediaItemIndex + 1)
        m.animationShowSecondary.delay = m.mediaItems[m.currentMediaItemIndex].timeoutSeconds
        m.animationShowSecondary.control = "start"
      end if
    end sub
    

    demo of image slide in