3

I'm working on a Pixi.js game in Coffeescript that mainly focuses on rotating hexes. Everything about the rotation is working correctly in terms of animating the rotation and the underlying board state recognizing the rotation. However, for certain hexes on the board (not all of them), rotating them causes other (not all) sprites on the stage to "shake". They momentarily shift to the side and then shift back again when the rotation finishes. I've tried searching for the problem all over; the one result I found was someone with a similar issue on a different site but had no answers: here.

I'm not sure where to start in terms of diagnosing the bug myself. If it's a issue with the canvas I could look there, but it may also be a Pixi issue. Here's a gif of the issue. When I click on the first hex (on right), many of the lines shake. When I click on the second hex (left), they do not.

The project is quite large at this point in terms of code. Here's the animation function, and helper methods:

## The frame count the window is on ##
@count = 0

### Moves the connector to the correct lit layer ###
@toLit = (connector) ->
  try
    @colorContainers[connector.color].unlit.removeChild(connector.panel)
  catch
  if @typeIsArray connector.color
    if connector.color.length > 0
      c = connector.color[0].toUpperCase()
    else
      c = Color.asString(Color.NONE).toUpperCase()
  else
    c = connector.color.toUpperCase()
  if c is Color.asString(Color.NONE).toUpperCase()
    @toUnlit(connector) ## Prevent lighting of color.NONE
  else
    @colorContainers[c.toUpperCase()].lit.addChild(connector.panel)
    connector.linked = true
  return

### Moves the connector to the correct unlit layer ###
@toUnlit = (connector) ->
  try
    @colorContainers[connector.color].lit.removeChild(connector.panel)
  catch
  if @typeIsArray connector.color
    if connector.color.length > 0
      c = connector.color[0]
    else
      c = Color.asString(Color.NONE)
  else
    c = connector.color
  if connector.hex? and connector.hex instanceof Crystal  
    @colorContainers[Color.asString(Color.NONE).toUpperCase()].unlit.addChild(connector.panel)
  else
    @colorContainers[c.toUpperCase()].unlit.addChild(connector.panel)
  connector.linked = false
  return

### Creates a frame offset for the each color ###
@colorOffset = {}
for c in Color.values()
  if not isNaN(c)
    c = Color.fromString(c).toUpperCase()
  else
    c = c.toUpperCase()
  @colorOffset[c] = Math.random() + 0.5

### Updates the pulse filter that controls lighting effects ###
@calcPulseFilter = (count) ->
  for col, val of @colorContainers
    pulse = val.lit.filters[0]
    cont = (count + val.lit.pulseOffset)/val.lit.pulseLength
    m = pulse.matrix
    m[0] = Math.abs(Math.sin(cont * 2 * Math.PI)) * 0.5 + 0.5
    m[5] = Math.abs(Math.sin(cont * 2 * Math.PI)) * 0.5 + 0.5
    m[10] = Math.abs(Math.sin(cont * 2 * Math.PI)) * 0.5 + 0.5
    m[15] = Math.abs(Math.sin(cont * 2 * Math.PI)) * 0.25 + 0.75
    pulse.matrix = m
  for cont in @goalContainer.children
    if cont.children.length >= 2 and cont.filters.length >= 2
      pulse = cont.filters[1]
      correspondCont = @colorContainers[cont.children[0].color.toUpperCase()].lit
      c = (count + correspondCont.pulseOffset)/correspondCont.pulseLength
      m = pulse.matrix
      if parseInt(cont.children[1].text.substring(0, 1)) >= parseInt(cont.children[1].text.substring(2))
        m[0] = Math.abs(Math.sin(c * 2 * Math.PI)) * 0.5 + 0.5
        m[5] = Math.abs(Math.sin(c * 2 * Math.PI)) * 0.5 + 0.5
        m[10] = Math.abs(Math.sin(c * 2 * Math.PI)) * 0.5 + 0.5
        m[15] = Math.abs(Math.sin(c * 2 * Math.PI)) * 0.25 + 0.75
      else
        m[0] = 1
        m[5] = 1
        m[10] = 1
        m[15] = 1
      pulse.matrix = m
  return

### The animation function. Called by pixi and requests to be recalled ###
@animate = () ->
    ## Color animation
    window.count += 1;  ## Frame count
    @calcPulseFilter(window.count)
    rotSpeed = 1/5
    tolerance = 0.000001 ## For floating point errors - difference below this is considered 'equal'
    radTo60Degree = 1.04719755 ## 1 radian * this coefficient = 60 degrees
    if (@BOARD?)
      ## Update text on goal
      curLit = @BOARD.crystalLitCount()
      goalContainer = @menu.children[@goalContainerIndex]
      isWin = true ## True if this user has won - every goal set.
      for pan in goalContainer.children
        for spr in pan.children
          if spr instanceof PIXI.Text and spr.color.toUpperCase() of curLit
            spr.setText(curLit[spr.color.toUpperCase()] + spr.text.substring(1))
            if curLit[spr.color.toUpperCase()] < parseInt(spr.text.substring(2))
              isWin = false

      if isWin and (not @winContainer?) and @showWinContainer
        @gameOn = false
        @makeWinGameContainer()

      for h in @BOARD.allHexes()
        ##Update lighting of all hexes
        if h.isLit().length > 0 and not h.backPanel.children[0].lit
          h.backPanel.children[0].lit = true
          if not (h instanceof Prism)
            @toLit(h.backPanel.spr)
        if h.isLit().length is 0 and h.backPanel.children[0].lit
          h.backPanel.children[0].lit = false
          if not (h instanceof Prism)
            @toUnlit(h.backPanel.spr)

        hLit = h.isLit()
        if h instanceof Prism
          ## Adjust opacity of cores
          for col, core of h.cores
            if col.toLowerCase() not in hLit and core.alpha > 0
              core.alpha = 0
            else if col.toLowerCase() in hLit and core.alpha is 0
              core.alpha = 0.75

        nS = h.getNeighborsWithBlanks()
        ## Fixing lighting of connectors 
        for panel in h.colorPanels
          col = panel.color.toLowerCase()
          for connector in panel.children
            for side in connector.sides
              n = nS[side]

              if n? and col in hLit and n.colorOfSide(n.indexLinked(h)) is col and not connector.linked
                @toLit(connector)
                for nPanel in n.colorPanels
                  for nConnector in nPanel.children
                    for nSide in nConnector.sides
                      if nSide is n.indexLinked(h) and not nConnector.linked
                        @toLit(nConnector)
              else if connector.linked and col not in hLit
                @toUnlit(connector)
                if n?
                  for nPanel in n.colorPanels
                    for nConnector in nPanel.children
                      for nSide in nConnector.sides
                        if nSide is n.indexLinked(h) and not nConnector.linked
                          @toUnlit(nConnector)

        ### Rotation of a prism - finds a prism that wants to rotate and rotates it a bit. ###
        ### If this is the first notification that this prism wants to rotate, stops providing light. ###
        ### If the prism is now done rotating, starts providing light again ###
        if h instanceof Prism and h.currentRotation isnt h.targetRotation
          if h.canLight
            h.canLight = false
            h.light()
          inc = 
            if (h.targetRotation - h.prevRotation) >= 0 
              rotSpeed
            else
              -rotSpeed
          h.backPanel.rotation += inc * radTo60Degree
          h.currentRotation += inc 
          for value in h.colorPanels
            value.rotation += inc * radTo60Degree
          if Math.abs(h.targetRotation - h.currentRotation) < tolerance
            inc = (h.targetRotation - h.currentRotation)
            h.backPanel.rotation += inc * radTo60Degree
            h.currentRotation += inc
            for value in h.colorPanels
              value.rotation += inc * radTo60Degree
              ## Update side index of each sprite
              for spr in value.children
                newSides = []
                for side in spr.sides
                  newSides.push((side + (h.currentRotation - h.prevRotation)) %% Hex.SIDES)
                spr.sides = newSides
            h.prevRotation = h.currentRotation
            h.canLight = true
            h.light()

        ### Spark and crystal color changing ###
        if (h instanceof Spark or h instanceof Crystal) and h.toColor isnt ""
          col = if (not isNaN(h.toColor)) 
                  Color.asString(h.toColor).toUpperCase() 
                else 
                  h.toColor.toUpperCase()
          h.backPanel.spr.color = col
          @toLit(h.backPanel.spr)
          h.toColor = ""
    requestAnimFrame(animate )
    @renderer.render(@stage)
    return

If there are any other parts of the code you think are relevant to the bug let me know and I'll put those parts up. Any help or suggestions would be really great!

Mshnik
  • 7,032
  • 1
  • 25
  • 38

1 Answers1

6

Stumbled upon the solution on a whim - when you rotate a sprite that is currently the at the border of its parent DisplayObjectContainer(), it causes the parent container to resize. It continues to resize throughout the rotation, as the sprite that defines the corner is moving. The change in dimension of the container causes it to shift relative to its siblings.

The fix is to add an invisible, static sprite to the container in which you are rotating things that is slightly larger than the container would be otherwise. That way you're guaranteed that the sprite you are rotating isn't one of the edges and thus won't cause the container to resize.

Mshnik
  • 7,032
  • 1
  • 25
  • 38