Fixed ClearRenderer method

Fixed ClearRenderer method

Strict

Import mojo

Import flxextern
Import flxpoint
Import flxobject
Import flxcamera
Import flxg

Import system.flxanim
Import system.flxcolor

Import "data/default_flx.png"

Class FlxSprite Extends FlxObject

	Global ClassObject:Object

	Field origin:FlxPoint

	Field offset:FlxPoint

	Field scale:FlxPoint

	Field blend:Int

	Field finished:Bool

	Field frameWidth:Int

	Field frameHeight:Int

	Field frames:Int

	Field dirty:Bool

	Field _color:FlxColor

	Field _mixedColor:FlxColor

	Field _camera:FlxCamera

Private
	Global _GraphicLoader:FlxGraphicLoader = New FlxGraphicLoader()

	Global _Matrix:Float[6]

	Field _animations:StringMap<FlxAnim>

	Field _flipped:Bool

	Field _flipNeeded:Bool

	Field _curAnim:FlxAnim

	Field _curFrame:Int

	Field _curIndex:Int

	Field _frameTimer:Float

	Field _paused:Bool

	Field _callback:FlxAnimationListener

	Field _facing:Int

	Field _alpha:Float

	Field _bakedRotation:Float

	Field _pixels:Image

	Field _halfWidth:Float

	Field _halfHeight:Float

	Field _renderer:FlxSpriteRenderer

Public
	Method New(x:Float = 0, y:Float = 0, simpleGraphic:String = "")
		Super.New(x, y)

		offset = New FlxPoint()
		origin = New FlxPoint()

		scale = New FlxPoint(1.0, 1.0)
		_alpha = 1
		_color = New FlxColor(FlxG.WHITE)
		blend = AlphaBlend
		_cameras = Null

		finished = False
		_facing = RIGHT
		_animations = New StringMap<FlxAnim>
		_flipped = False
		_curAnim = Null
		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0
		_paused = False

		_callback = Null

		_mixedColor = New FlxColor(FlxG.WHITE)

		If (simpleGraphic.Length() = 0)
			simpleGraphic = "default" + FlxG.DATA_SUFFIX
		End If

		LoadGraphic(simpleGraphic)
	End Method

	Method Destroy:Void()
		If (_animations <> Null) Then
			_animations.Clear()
			_animations = Null
		End If

		offset = Null
		origin = Null
		scale = Null
		_curAnim = Null
		_callback = Null
		_color = Null
		_mixedColor = Null
		_camera = Null
		_renderer = Null

		Super.Destroy()
	End Method

	Method LoadGraphic:FlxSprite(graphic:String, animated:Bool = False, reverse:Bool = False, width:Int = 0, height:Int = 0, unique:Bool = False)
		_bakedRotation = 0

		_GraphicLoader.name = graphic
		_GraphicLoader.animated = animated
		_GraphicLoader.width = width
		_GraphicLoader.height = height

		_pixels = FlxG.AddBitmap(graphic, _GraphicLoader, unique)

		_flipped = reverse

		Self.width = _pixels.Width()
		frameWidth = Self.width

		Self.height = _pixels.Height()
		frameHeight = Self.height

		ResetHelpers()

		Return Self
	End Method

	Method LoadRotatedGraphic:FlxSprite(graphic:String, rotations:Int = 16, frame:Int = -1)
		Error "LoadRotatedGraphic not currenlty support in " + FlxG.LIBRARY_NAME
		'TODO
		Return Null
	End Method

	Method MakeGraphic:FlxSprite(width:Int, height:Int, color:Int = FlxG.WHITE, unique:Bool = False, key:String = "")
		_pixels = FlxG.CreateBitmap(width, height, color, unique, key)
		_bakedRotation = 0

		Self.width = width
		frameWidth = width
		Self.height = height
		frameHeight = height

		ResetHelpers()

		Return Self
	End Method

	Method PostUpdate:Void()
		Super.PostUpdate()
		_UpdateAnimation()
	End Method

	Method Draw:Void()
		If (_flickerTimer <> 0) Then
			_flicker = Not _flicker
			If (_flicker) Return
		End If

		_camera = FlxG._CurrentCamera

		If (_cameras <> Null And Not _cameras.Contains(_camera)) Return
		If (Not OnScreen(_camera)) Return

		If (dirty) _CalcFrame()

		If (FlxG._LastDrawingBlend <> blend) Then
			SetBlend(blend)
			FlxG._LastDrawingBlend = blend
		End If

		_point.x = x - int(_camera.scroll.x * scrollFactor.x) - offset.x
		_point.y = y - int(_camera.scroll.y * scrollFactor.y) - offset.y

		If (_point.x > 0) Then
			_point.x += 0.0000001
		Else
			_point.x -= 0.0000001
		End If

		If (_point.y > 0) Then
			_point.y += 0.0000001
		Else
			_point.y -= 0.0000001
		End If

		If (_camera.Color <> FlxG.WHITE) Then
			_mixedColor.MixRGB(_color, _camera._color)

			If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
				SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
				FlxG._LastDrawingColor = _mixedColor.argb
			End If
		Else
			If (FlxG._LastDrawingColor <> _color.argb) Then
				SetColor(_color.r, _color.g, _color.b)
				FlxG._LastDrawingColor = _color.argb
			End If
		End If

		If (_camera.Alpha < 1) Then
			Local _mixedAlpha:Float = _camera.Alpha * _alpha

			If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
				SetAlpha(_mixedAlpha)
				FlxG._LastDrawingAlpha = _mixedAlpha
			End If
		Else
			If (FlxG._LastDrawingAlpha <> _alpha) Then
				SetAlpha(_alpha)
				FlxG._LastDrawingAlpha = _alpha
			End If
		End If

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			If ( Not _flipNeeded) Then
				If (_renderer = Null) Then
					DrawImage(_pixels, _point.x, _point.y, _curIndex)
				Else
					_renderer.OnSpriteRender(_point.x, _point.y)
				End If
			Else
				PushMatrix()
					Transform(-1, 0, 0, 1, _point.x + _halfWidth, _point.y + _halfHeight)

					If (_renderer = Null) Then
						DrawImage(_pixels, -_halfWidth, -_halfHeight, _curIndex)
					Else
						_renderer.OnSpriteRender(-_halfWidth, -_halfHeight)
					End If
				PopMatrix()
			End If
		Else
			PushMatrix()
				'Translate
				_Matrix[4] = _point.x + origin.x
				_Matrix[5] = _point.y + origin.y

				'Scale
				_Matrix[0] = scale.x
				_Matrix[3] = scale.y

				'Rotate
				If (angle <> 0 And _bakedRotation = 0) Then
					Local sin:Float = Sin(angle)
					Local cos:Float = Cos(angle)

					_Matrix[1] = sin * _Matrix[0]
					_Matrix[2] = -sin * _Matrix[3]
					_Matrix[0] *= cos
					_Matrix[3] *= cos
				Else
					_Matrix[1] = 0
					_Matrix[2] = 0
				End If

				Transform(_Matrix[0], _Matrix[1], _Matrix[2], _Matrix[3], _Matrix[4], _Matrix[5])

				If (_flipNeeded) Then
					Transform(-1, 0, 0, 1, _halfWidth - origin.x, _halfHeight - origin.y)
				End If

				If (_renderer = Null) Then
					DrawImage(_pixels, -origin.x, -origin.y, _curIndex)
				Else
					_renderer.OnSpriteRender(-origin.x, -origin.y)
				End If
			PopMatrix()
		End If

	#If FLX_DEBUG_ENABLED = "1"
		_VisibleCount += 1;
		If (FlxG.VisualDebug And Not ignoreDrawDebug) DrawDebug(_camera);
	#End
	End Method

	Method DrawFrame:Void(force:Bool = False)
		If (force Or dirty) _CalcFrame()
	End Method

	Method AddAnimation:FlxAnim(name:String, frames:Int[], frameRate:Float = 0, looped:Bool = True)
		Local anim:FlxAnim = New FlxAnim(name, frames, frameRate, looped)
		_animations.Set(name, anim)

		Return anim
	End Method

	Method AddAnimation:FlxAnim(name:String, startFrame:Int, endFrame:Int, frameRate:Float = 0, looped:Bool = True)
		Local i:Int = 0
		Local l:Int = endFrame - startFrame + 1
		Local frames:Int[] = New Int[l]

		While (i < l)
			frames[i] = startFrame + i
			i += 1
		Wend

		Return AddAnimation(name, frames, frameRate, looped)
	End Method

	Method AddAnimation:FlxAnim(name:String, frameRate:Float = 0, looped:Bool = True)
		If (_pixels = Null) Return Null
		Return AddAnimation(name, 0, _pixels.Frames() -1, frameRate, looped)
	End Method

	Method AddAnimationCallback:Void(animationCallback:FlxAnimationListener)
		_callback = animationCallback
	End Method

	Method ClearAnimationCallback:Void()
		_callback = Null
	End Method

	Method GetAnimation:FlxAnim(animName:String)
		Return _animations.Get(animName)
	End Method

	Method GetAnimation:FlxAnim()
		Return _curAnim
	End Method

	Method Play:Void(animName:String, force:Bool = False)
		If (Not force And _curAnim <> Null And animName = _curAnim.name And( _curAnim.looped Or Not finished )) Return

		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0
		_paused = False

		Local anim:FlxAnim = _animations.Get(animName)

		If (anim <> Null) Then
			_curAnim = anim

			If (_curAnim.delay <= 0) Then
				finished = True
			Else
				finished = False
			End If

			_curIndex = _curAnim.frames[_curFrame]
			dirty = True
			Return
		End If

		#If FLX_DEBUG_ENABLED = "1"
			FlxG.Log("WARNING: No animation called ~q" + animName + "~q")
		#End
	End Method

	Method Stop:Void()
		Frame = 0
	End Method

	Method Pause:Void()
		_paused = True
	End Method

	Method Resume:Void()
		_paused = False
	End Method

	Method RandomFrame:Void()
		_curAnim = Null

		If (_pixels <> Null) Then
			_curIndex = Int(FlxG.Random() * (_pixels.Frames() - 1))
		Else
			_curIndex = 0
		End If

		dirty = True
	End Method

	Method SetOriginToCorner:Void()
		origin.x = 0
		origin.y = 0
	End Method

	Method CenterOffsets:Void(adjustPosition:Bool = False)
		offset.x = (frameWidth - width) * .5
		offset.y = (frameHeight - height) * .5

		If (adjustPosition) Then
			x += offset.x
			y += offset.y
		End If
	End Method

	Method Pixels:Image() Property
		Return _pixels
	End Method

	Method Pixels:Void(pixels:Image) Property
		_pixels = pixels

		Local width:Int = 0
		Local height:Int = 0

		If (_pixels <> Null) Then
			width = pixels.Width()
			height = pixels.Height()
		End If

		If (_pixels <> Null Or _renderer = Null) Then
			Self.width = width
			Self.height = height
			frameWidth = width
			frameHeight = height
			ResetHelpers()
		End If
	End Method

	Method Facing:Int() Property
		Return _facing
	End Method

	Method Facing:Void(direction:Int) Property
		If (_facing <> direction) dirty = True
		_facing = direction

		_flipNeeded = (_flipped And _facing = LEFT)
	End Method

	Method Alpha:Float() Property
		Return _alpha
	End Method

	Method Alpha:Void(alpha:Float) Property
		If (alpha < 0) alpha = 0
		_alpha = alpha
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetRGB(color)
	End Method

	Method Frame:Int() Property
		Return _curIndex
	End Method

	Method Frame:Void(frame:Int) Property
		_curAnim = Null
		_curIndex = frame
		_paused = False
		dirty = True
	End Method

	Method OnScreen:Bool(camera:FlxCamera = Null)
		If (camera = Null) camera = FlxG.Camera
		GetScreenXY(_point, camera)

		_point.x = _point.x - offset.x
		_point.y = _point.y - offset.y

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			Return _point.x + frameWidth > 0 And _point.x < camera.Width And _point.y + frameHeight > 0 And _point.y < camera.Height
		End If

		Local radius:Float = Sqrt(_halfWidth * _halfWidth + _halfHeight * _halfHeight) * Max(Abs(scale.x), Abs(scale.y))
		_point.x += _halfWidth
		_point.y += _halfHeight

		Return _point.x + radius > 0 And _point.x - radius < camera.Width And _point.y + radius > 0 And _point.y - radius < camera.Height
	End Method

	Method SetRenderer:Void(render:FlxSpriteRenderer)
		_renderer = render
		_renderer.OnSpriteBind(Self)

		frameWidth = width
		frameHeight = height

		ResetHelpers()
	End Method

	Method ClearRenderer:Void()
		Local renderer:FlxSpriteRenderer = _renderer
		_renderer = Null
		renderer.OnSpriteUnbind()

		frameWidth = width
		frameHeight = height

		ResetHelpers()
	End Method

	Method ResetHelpers:Void()
		_halfWidth = frameWidth * .5
		_halfHeight = frameHeight * .5
		origin.Make(_halfWidth, _halfHeight)

		If (_pixels <> Null) Then
			frames = _pixels.Frames()
		Else
			frames = 1
		End If

		_curIndex = 0
		_mixedColor.SetARGB(FlxG.WHITE)
	End Method

Private
	Method _UpdateAnimation:Void()
		If (_bakedRotation > 0) Then
			Local oldIndex:Int = _curIndex
			Local angleHelper:Int = angle Mod 360

			If (angleHelper < 0) angleHelper += 360
			_curIndex = angleHelper / _bakedRotation + 0.5

			If (oldIndex <> _curIndex) _CalcFrame()
			Return
		End If

		If (_paused) Return

		If (_curAnim <> Null And _curAnim.delay > 0 And (_curAnim.looped Or Not finished))
			_frameTimer += FlxG.Elapsed
			While (_frameTimer > _curAnim.delay)
				_frameTimer -= _curAnim.delay

				If (_curFrame = _curAnim.frames.Length() - 1) Then
					If (_curAnim.looped) _curFrame = 0
					finished = True
				Else
					_curFrame += 1
				End If

				_curIndex = _curAnim.frames[_curFrame]
				dirty = True
			Wend
		End If

		If (dirty) _CalcFrame()
	End Method

	Method _CalcFrame:Void()
		If (_callback <> Null) Then
			If (_curAnim <> Null) Then
				_callback.OnAnimationFrame(_curAnim, _curFrame, _curIndex)
			Else
				_callback.OnAnimationFrame(Null, _curFrame, _curIndex)
			End If
		End If
		dirty = False
	End Method

End Class

Interface FlxSpriteRenderer

	Method OnSpriteBind:Void(sprite:FlxSprite)

	Method OnSpriteUnbind:Void()

	Method OnSpriteRender:Void(x:Float, y:Float)

End Interface

Private
Class FlxGraphicLoader Extends FlxResourceLoader<Image>

	Field name:String
	Field animated:Bool
	Field width:Float
	Field height:Float

	Method Load:Image(name:String)
		Local image:Image = LoadImage(FlxAssetsManager.GetImagePath(Self.name))

		If (Not animated) Then
			Return image
		Else
			Local frames:Int

			If (width = 0 And height = 0) Then
				width = image.Height()
			ElseIf (width = 0) Then
				width = height
			ElseIf (height = 0) Then
				height = width
			End If

			If (height = 0) Then
				frames = Ceil(image.Width() / width)
				height = width
			Else
				frames = Ceil((image.Width() * image.Height()) / (width * height))
			End If

			Return image.GrabImage(0, 0, width, height, frames)
		End If
	End Method

End Class

Strict

Import mojo

Import flxextern
Import flxpoint
Import flxobject
Import flxcamera
Import flxg

Import system.flxanim
Import system.flxcolor

Import "data/default_flx.png"

Class FlxSprite Extends FlxObject

	Global ClassObject:Object

	Field origin:FlxPoint

	Field offset:FlxPoint

	Field scale:FlxPoint

	Field blend:Int

	Field finished:Bool

	Field frameWidth:Int

	Field frameHeight:Int

	Field frames:Int

	Field dirty:Bool

	Field _color:FlxColor

	Field _mixedColor:FlxColor

	Field _camera:FlxCamera

Private
	Global _GraphicLoader:FlxGraphicLoader = New FlxGraphicLoader()

	Global _Matrix:Float[6]

	Field _animations:StringMap<FlxAnim>

	Field _flipped:Bool

	Field _flipNeeded:Bool

	Field _curAnim:FlxAnim

	Field _curFrame:Int

	Field _curIndex:Int

	Field _frameTimer:Float

	Field _paused:Bool

	Field _callback:FlxAnimationListener

	Field _facing:Int

	Field _alpha:Float

	Field _bakedRotation:Float

	Field _pixels:Image

	Field _halfWidth:Float

	Field _halfHeight:Float

	Field _renderer:FlxSpriteRenderer

Public
	Method New(x:Float = 0, y:Float = 0, simpleGraphic:String = "")
		Super.New(x, y)

		offset = New FlxPoint()
		origin = New FlxPoint()

		scale = New FlxPoint(1.0, 1.0)
		_alpha = 1
		_color = New FlxColor(FlxG.WHITE)
		blend = AlphaBlend
		_cameras = Null

		finished = False
		_facing = RIGHT
		_animations = New StringMap<FlxAnim>
		_flipped = False
		_curAnim = Null
		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0
		_paused = False

		_callback = Null

		_mixedColor = New FlxColor(FlxG.WHITE)

		If (simpleGraphic.Length() = 0)
			simpleGraphic = "default" + FlxG.DATA_SUFFIX
		End If

		LoadGraphic(simpleGraphic)
	End Method

	Method Destroy:Void()
		If (_animations <> Null) Then
			_animations.Clear()
			_animations = Null
		End If

		offset = Null
		origin = Null
		scale = Null
		_curAnim = Null
		_callback = Null
		_color = Null
		_mixedColor = Null
		_camera = Null
		_renderer = Null

		Super.Destroy()
	End Method

	Method LoadGraphic:FlxSprite(graphic:String, animated:Bool = False, reverse:Bool = False, width:Int = 0, height:Int = 0, unique:Bool = False)
		_bakedRotation = 0

		_GraphicLoader.name = graphic
		_GraphicLoader.animated = animated
		_GraphicLoader.width = width
		_GraphicLoader.height = height

		_pixels = FlxG.AddBitmap(graphic, _GraphicLoader, unique)

		_flipped = reverse

		Self.width = _pixels.Width()
		frameWidth = Self.width

		Self.height = _pixels.Height()
		frameHeight = Self.height

		ResetHelpers()

		Return Self
	End Method

	Method LoadRotatedGraphic:FlxSprite(graphic:String, rotations:Int = 16, frame:Int = -1)
		Error "LoadRotatedGraphic not currenlty support in " + FlxG.LIBRARY_NAME
		'TODO
		Return Null
	End Method

	Method MakeGraphic:FlxSprite(width:Int, height:Int, color:Int = FlxG.WHITE, unique:Bool = False, key:String = "")
		_pixels = FlxG.CreateBitmap(width, height, color, unique, key)
		_bakedRotation = 0

		Self.width = width
		frameWidth = width
		Self.height = height
		frameHeight = height

		ResetHelpers()

		Return Self
	End Method

	Method PostUpdate:Void()
		Super.PostUpdate()
		_UpdateAnimation()
	End Method

	Method Draw:Void()
		If (_flickerTimer <> 0) Then
			_flicker = Not _flicker
			If (_flicker) Return
		End If

		_camera = FlxG._CurrentCamera

		If (_cameras <> Null And Not _cameras.Contains(_camera)) Return
		If (Not OnScreen(_camera)) Return

		If (dirty) _CalcFrame()

		If (FlxG._LastDrawingBlend <> blend) Then
			SetBlend(blend)
			FlxG._LastDrawingBlend = blend
		End If

		_point.x = x - int(_camera.scroll.x * scrollFactor.x) - offset.x
		_point.y = y - int(_camera.scroll.y * scrollFactor.y) - offset.y

		If (_point.x > 0) Then
			_point.x += 0.0000001
		Else
			_point.x -= 0.0000001
		End If

		If (_point.y > 0) Then
			_point.y += 0.0000001
		Else
			_point.y -= 0.0000001
		End If

		If (_camera.Color <> FlxG.WHITE) Then
			_mixedColor.MixRGB(_color, _camera._color)

			If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
				SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
				FlxG._LastDrawingColor = _mixedColor.argb
			End If
		Else
			If (FlxG._LastDrawingColor <> _color.argb) Then
				SetColor(_color.r, _color.g, _color.b)
				FlxG._LastDrawingColor = _color.argb
			End If
		End If

		If (_camera.Alpha < 1) Then
			Local _mixedAlpha:Float = _camera.Alpha * _alpha

			If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
				SetAlpha(_mixedAlpha)
				FlxG._LastDrawingAlpha = _mixedAlpha
			End If
		Else
			If (FlxG._LastDrawingAlpha <> _alpha) Then
				SetAlpha(_alpha)
				FlxG._LastDrawingAlpha = _alpha
			End If
		End If

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			If ( Not _flipNeeded) Then
				If (_renderer = Null) Then
					DrawImage(_pixels, _point.x, _point.y, _curIndex)
				Else
					_renderer.OnSpriteRender(_point.x, _point.y)
				End If
			Else
				PushMatrix()
					Transform(-1, 0, 0, 1, _point.x + _halfWidth, _point.y + _halfHeight)

					If (_renderer = Null) Then
						DrawImage(_pixels, -_halfWidth, -_halfHeight, _curIndex)
					Else
						_renderer.OnSpriteRender(-_halfWidth, -_halfHeight)
					End If
				PopMatrix()
			End If
		Else
			PushMatrix()
				'Translate
				_Matrix[4] = _point.x + origin.x
				_Matrix[5] = _point.y + origin.y

				'Scale
				_Matrix[0] = scale.x
				_Matrix[3] = scale.y

				'Rotate
				If (angle <> 0 And _bakedRotation = 0) Then
					Local sin:Float = Sin(angle)
					Local cos:Float = Cos(angle)

					_Matrix[1] = sin * _Matrix[0]
					_Matrix[2] = -sin * _Matrix[3]
					_Matrix[0] *= cos
					_Matrix[3] *= cos
				Else
					_Matrix[1] = 0
					_Matrix[2] = 0
				End If

				Transform(_Matrix[0], _Matrix[1], _Matrix[2], _Matrix[3], _Matrix[4], _Matrix[5])

				If (_flipNeeded) Then
					Transform(-1, 0, 0, 1, _halfWidth - origin.x, _halfHeight - origin.y)
				End If

				If (_renderer = Null) Then
					DrawImage(_pixels, -origin.x, -origin.y, _curIndex)
				Else
					_renderer.OnSpriteRender(-origin.x, -origin.y)
				End If
			PopMatrix()
		End If

	#If FLX_DEBUG_ENABLED = "1"
		_VisibleCount += 1;
		If (FlxG.VisualDebug And Not ignoreDrawDebug) DrawDebug(_camera);
	#End
	End Method

	Method DrawFrame:Void(force:Bool = False)
		If (force Or dirty) _CalcFrame()
	End Method

	Method AddAnimation:FlxAnim(name:String, frames:Int[], frameRate:Float = 0, looped:Bool = True)
		Local anim:FlxAnim = New FlxAnim(name, frames, frameRate, looped)
		_animations.Set(name, anim)

		Return anim
	End Method

	Method AddAnimation:FlxAnim(name:String, startFrame:Int, endFrame:Int, frameRate:Float = 0, looped:Bool = True)
		Local i:Int = 0
		Local l:Int = endFrame - startFrame + 1
		Local frames:Int[] = New Int[l]

		While (i < l)
			frames[i] = startFrame + i
			i += 1
		Wend

		Return AddAnimation(name, frames, frameRate, looped)
	End Method

	Method AddAnimation:FlxAnim(name:String, frameRate:Float = 0, looped:Bool = True)
		If (_pixels = Null) Return Null
		Return AddAnimation(name, 0, _pixels.Frames() -1, frameRate, looped)
	End Method

	Method AddAnimationCallback:Void(animationCallback:FlxAnimationListener)
		_callback = animationCallback
	End Method

	Method ClearAnimationCallback:Void()
		_callback = Null
	End Method

	Method GetAnimation:FlxAnim(animName:String)
		Return _animations.Get(animName)
	End Method

	Method GetAnimation:FlxAnim()
		Return _curAnim
	End Method

	Method Play:Void(animName:String, force:Bool = False)
		If (Not force And _curAnim <> Null And animName = _curAnim.name And( _curAnim.looped Or Not finished )) Return

		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0
		_paused = False

		Local anim:FlxAnim = _animations.Get(animName)

		If (anim <> Null) Then
			_curAnim = anim

			If (_curAnim.delay <= 0) Then
				finished = True
			Else
				finished = False
			End If

			_curIndex = _curAnim.frames[_curFrame]
			dirty = True
			Return
		End If

		#If FLX_DEBUG_ENABLED = "1"
			FlxG.Log("WARNING: No animation called ~q" + animName + "~q")
		#End
	End Method

	Method Stop:Void()
		Frame = 0
	End Method

	Method Pause:Void()
		_paused = True
	End Method

	Method Resume:Void()
		_paused = False
	End Method

	Method RandomFrame:Void()
		_curAnim = Null

		If (_pixels <> Null) Then
			_curIndex = Int(FlxG.Random() * (_pixels.Frames() - 1))
		Else
			_curIndex = 0
		End If

		dirty = True
	End Method

	Method SetOriginToCorner:Void()
		origin.x = 0
		origin.y = 0
	End Method

	Method CenterOffsets:Void(adjustPosition:Bool = False)
		offset.x = (frameWidth - width) * .5
		offset.y = (frameHeight - height) * .5

		If (adjustPosition) Then
			x += offset.x
			y += offset.y
		End If
	End Method

	Method Pixels:Image() Property
		Return _pixels
	End Method

	Method Pixels:Void(pixels:Image) Property
		_pixels = pixels

		Local width:Int = 0
		Local height:Int = 0

		If (_pixels <> Null) Then
			width = pixels.Width()
			height = pixels.Height()
		End If

		If (_pixels <> Null Or _renderer = Null) Then
			Self.width = width
			Self.height = height
			frameWidth = width
			frameHeight = height
			ResetHelpers()
		End If
	End Method

	Method Facing:Int() Property
		Return _facing
	End Method

	Method Facing:Void(direction:Int) Property
		If (_facing <> direction) dirty = True
		_facing = direction

		_flipNeeded = (_flipped And _facing = LEFT)
	End Method

	Method Alpha:Float() Property
		Return _alpha
	End Method

	Method Alpha:Void(alpha:Float) Property
		If (alpha < 0) alpha = 0
		_alpha = alpha
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetRGB(color)
	End Method

	Method Frame:Int() Property
		Return _curIndex
	End Method

	Method Frame:Void(frame:Int) Property
		_curAnim = Null
		_curIndex = frame
		_paused = False
		dirty = True
	End Method

	Method OnScreen:Bool(camera:FlxCamera = Null)
		If (camera = Null) camera = FlxG.Camera
		GetScreenXY(_point, camera)

		_point.x = _point.x - offset.x
		_point.y = _point.y - offset.y

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			Return _point.x + frameWidth > 0 And _point.x < camera.Width And _point.y + frameHeight > 0 And _point.y < camera.Height
		End If

		Local radius:Float = Sqrt(_halfWidth * _halfWidth + _halfHeight * _halfHeight) * Max(Abs(scale.x), Abs(scale.y))
		_point.x += _halfWidth
		_point.y += _halfHeight

		Return _point.x + radius > 0 And _point.x - radius < camera.Width And _point.y + radius > 0 And _point.y - radius < camera.Height
	End Method

	Method SetRenderer:Void(render:FlxSpriteRenderer)
		_renderer = render
		_renderer.OnSpriteBind(Self)

		frameWidth = width
		frameHeight = height

		ResetHelpers()
	End Method

	Method ClearRenderer:Void()
		_renderer.OnSpriteUnbind()
		_renderer = Null

		frameWidth = width
		frameHeight = height

		ResetHelpers()
	End Method

	Method ResetHelpers:Void()
		_halfWidth = frameWidth * .5
		_halfHeight = frameHeight * .5
		origin.Make(_halfWidth, _halfHeight)

		If (_pixels <> Null) Then
			frames = _pixels.Frames()
		Else
			frames = 1
		End If

		_curIndex = 0
		_mixedColor.SetARGB(FlxG.WHITE)
	End Method

Private
	Method _UpdateAnimation:Void()
		If (_bakedRotation > 0) Then
			Local oldIndex:Int = _curIndex
			Local angleHelper:Int = angle Mod 360

			If (angleHelper < 0) angleHelper += 360
			_curIndex = angleHelper / _bakedRotation + 0.5

			If (oldIndex <> _curIndex) _CalcFrame()
			Return
		End If

		If (_paused) Return

		If (_curAnim <> Null And _curAnim.delay > 0 And (_curAnim.looped Or Not finished))
			_frameTimer += FlxG.Elapsed
			While (_frameTimer > _curAnim.delay)
				_frameTimer -= _curAnim.delay

				If (_curFrame = _curAnim.frames.Length() - 1) Then
					If (_curAnim.looped) _curFrame = 0
					finished = True
				Else
					_curFrame += 1
				End If

				_curIndex = _curAnim.frames[_curFrame]
				dirty = True
			Wend
		End If

		If (dirty) _CalcFrame()
	End Method

	Method _CalcFrame:Void()
		If (_callback <> Null) Then
			If (_curAnim <> Null) Then
				_callback.OnAnimationFrame(_curAnim, _curFrame, _curIndex)
			Else
				_callback.OnAnimationFrame(Null, _curFrame, _curIndex)
			End If
		End If
		dirty = False
	End Method

End Class

Interface FlxSpriteRenderer

	Method OnSpriteBind:Void(sprite:FlxSprite)

	Method OnSpriteUnbind:Void()

	Method OnSpriteRender:Void(x:Float, y:Float)

End Interface

Private
Class FlxGraphicLoader Extends FlxResourceLoader<Image>

	Field name:String
	Field animated:Bool
	Field width:Float
	Field height:Float

	Method Load:Image(name:String)
		Local image:Image = LoadImage(FlxAssetsManager.GetImagePath(Self.name))

		If (Not animated) Then
			Return image
		Else
			Local frames:Int

			If (width = 0 And height = 0) Then
				width = image.Height()
			ElseIf (width = 0) Then
				width = height
			ElseIf (height = 0) Then
				height = width
			End If

			If (height = 0) Then
				frames = Ceil(image.Width() / width)
				height = width
			Else
				frames = Ceil((image.Width() * image.Height()) / (width * height))
			End If

			Return image.GrabImage(0, 0, width, height, frames)
		End If
	End Method

End Class


add MoveTo(Float, Float) to TimelineFactory

Strict

Private

Import bono

Public

Class TimelineFactory
    Private

    Field obj:Object
    Field line:Timeline

    Public

    Method New()
        Throw New InvalidConstructorException("use New(Object)")
    End

    Method New(obj:Object)
        Self.obj = obj
        line = New Timeline()
    End

    Method GetTimeline:Timeline()
        Return line
    End

    Method Loop:Void(times:Int=Timeline.INFINITE)
        line.Loop(times)
    End

    Method Parallel:ActionParallel()
        Local result := New ActionParallel(obj)
        line.Append(result)
        Return result
    End

    Method Callback:ActionCallback(name:String, cb:ActionsCallback)
        Local result := New ActionCallback(obj, name, cb)
        line.Append(result)
        Return result
    End

    Method MoveTo:ActionMoveTo(pos:Vector2D)
        Local result := New ActionMoveTo(Positionable(obj), pos)
        line.Append(result)
        Return result
    End

    Method MoveTo:ActionMoveTo(x:Float, y:Float)
        Return MoveTo(New Vector2D(x, y))
    End

    Method FadeAlphaTo:ActionFadeAlphaTo(alpha:Float)
        Local result := New ActionFadeAlphaTo(Colorable(obj), alpha)
        line.Append(result)
        Return result
    End

    Method Scale:ActionScale(scaleToAdd:Vector2D)
        Local result := New ActionScale(Sprite(obj), scaleToAdd)
        line.Append(result)
        Return result
    End

    Method Scale:ActionScale(val:Float)
        Return Scale(val, val)
    End

    Method Scale:ActionScale(x:Float, y:Float)
        Return Scale(New Vector2D(x, y))
    End

    Method Sleep:ActionSleep(duration:Float)
        Local result := New ActionSleep(duration)
        line.Append(result)
        Return result
    End

    Method SleepRandom:ActionSleepRandom(minDuration:Float, maxDuration:Float)
        Return Sleep(Rnd(minDuration, maxDuration))
    End

    Method SleepRandomLateEval:ActionSleepRandomLateEval(minDuration:Float, maxDuration:Float)
        Local result := New ActionSleepRandomLateEval(minDuration, maxDuration)
        line.Append(result)
        Return result
    End

    Method Rotate:ActionRotate(angel:Float)
        Local result := New ActionRotate(Rotateable(obj), angel)
        line.Append(result)
        Return result
    End
End

Strict

Private

Import bono

Public

Class TimelineFactory
    Private

    Field obj:Object
    Field line:Timeline

    Public

    Method New()
        Throw New InvalidConstructorException("use New(Object)")
    End

    Method New(obj:Object)
        Self.obj = obj
        line = New Timeline()
    End

    Method GetTimeline:Timeline()
        Return line
    End

    Method Loop:Void(times:Int=Timeline.INFINITE)
        line.Loop(times)
    End

    Method Parallel:ActionParallel()
        Local result := New ActionParallel(obj)
        line.Append(result)
        Return result
    End

    Method Callback:ActionCallback(name:String, cb:ActionsCallback)
        Local result := New ActionCallback(obj, name, cb)
        line.Append(result)
        Return result
    End

    Method MoveTo:ActionMoveTo(pos:Vector2D)
        Local result := New ActionMoveTo(Positionable(obj), pos)
        line.Append(result)
        Return result
    End

    Method FadeAlphaTo:ActionFadeAlphaTo(alpha:Float)
        Local result := New ActionFadeAlphaTo(Colorable(obj), alpha)
        line.Append(result)
        Return result
    End

    Method Scale:ActionScale(scaleToAdd:Vector2D)
        Local result := New ActionScale(Sprite(obj), scaleToAdd)
        line.Append(result)
        Return result
    End

    Method Scale:ActionScale(val:Float)
        Return Scale(val, val)
    End

    Method Scale:ActionScale(x:Float, y:Float)
        Return Scale(New Vector2D(x, y))
    End

    Method Sleep:ActionSleep(duration:Float)
        Local result := New ActionSleep(duration)
        line.Append(result)
        Return result
    End

    Method SleepRandom:ActionSleepRandom(minDuration:Float, maxDuration:Float)
        Return Sleep(Rnd(minDuration, maxDuration))
    End

    Method SleepRandomLateEval:ActionSleepRandomLateEval(minDuration:Float, maxDuration:Float)
        Local result := New ActionSleepRandomLateEval(minDuration, maxDuration)
        line.Append(result)
        Return result
    End

    Method Rotate:ActionRotate(angel:Float)
        Local result := New ActionRotate(Rotateable(obj), angel)
        line.Append(result)
        Return result
    End
End


Fixed a couple of stupid storyboard bugs.

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import diddy.functions
Import diddy.containers
Import diddy.xml
Import diddy.format

Public
Import diddy.exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 7

Class Storyboard
Private
	Field sprites:DiddyStack<StoryboardSprite> = New DiddyStack<StoryboardSprite>
	Field sounds:DiddyStack<StoryboardSound> = New DiddyStack<StoryboardSound>
	Field musics:DiddyStack<StoryboardMusic> = New DiddyStack<StoryboardMusic>
	Field effects:DiddyStack<StoryboardEffect> = New DiddyStack<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String="storyboard.xml")
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile(filename)
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Push(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Push(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Push(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Push(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:DiddyStack<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:DiddyStack<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:DiddyStack<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:DiddyStack<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Count()
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(Self, currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Count()
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(Self, currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Count()
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(Self, currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Count()
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(Self, currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Count()
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Count()
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

Public
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	Method New()
		id = nextId
		nextId += 1
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:DiddyStack<StoryboardSpriteKeyframe> = New DiddyStack<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Push(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.CompareTo(other)
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Count()
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import diddy.functions
Import diddy.containers
Import diddy.xml
Import diddy.format

Public
Import diddy.exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 6

Class Storyboard
Private
	Field sprites:DiddyStack<StoryboardSprite> = New DiddyStack<StoryboardSprite>
	Field sounds:DiddyStack<StoryboardSound> = New DiddyStack<StoryboardSound>
	Field musics:DiddyStack<StoryboardMusic> = New DiddyStack<StoryboardMusic>
	Field effects:DiddyStack<StoryboardEffect> = New DiddyStack<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String)
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile("storyboard.xml")
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Push(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Push(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Push(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Push(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:DiddyStack<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:DiddyStack<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:DiddyStack<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:DiddyStack<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Count()
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(Self, currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Count()
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(Self, currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Count()
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(Self, currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Count()
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(Self, currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Count()
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Count()
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

Public
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	Method New()
		id = nextId
		nextId += 1
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:DiddyStack<StoryboardSpriteKeyframe> = New DiddyStack<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Push(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.CompareTo(other)
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Count()
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]


Added method for emit few signals and all signals

Strict

Import reflection
Import system.signals.flxsignal

Class FlxSignals

Private
	Field _signals:IntMap<FlxSignal>

	Field _methodSlots:List<FlxMethodSlot>

	Field _functionSlots:List<FlxFunctionSlot>

Public
	Method New()
		_signals = New IntMap<FlxSignal>()
		_methodSlots = New List<FlxMethodSlot>()
		_functionSlots = New List<FlxFunctionSlot>()
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener)
		_GetSignal(signalID).RegisterListener(listener)
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener, instant:Bool)
		_GetSignal(signalID).RegisterListener(listener, instant)
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener, instant:Bool, priority:Int)
		_GetSignal(signalID).RegisterListener(listener, instant, priority)
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object)
		Connect(signalID, _GetMethodSlot(methodInfo, context))
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object, instant:Bool)
		Connect(signalID, _GetMethodSlot(methodInfo, context), instant)
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object, instant:Bool, priority:Int)
		Connect(signalID, _GetMethodSlot(methodInfo, context), instant, priority)
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo)
		Connect(signalID, _GetFunctionSlot(functionInfo))
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo, instant:Bool)
		Connect(signalID, _GetFunctionSlot(functionInfo), instant)
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo, instant:Bool, priority:Int)
		Connect(signalID, _GetFunctionSlot(functionInfo), instant, priority)
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context))
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object, instant:Bool)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context), instant)
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object, instant:Bool, priority:Int)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context), instant, priority)
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])))
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String, instant:Bool)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])), instant)
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String, instant:Bool, priority:Int)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])), instant, priority)
	End Method

	Method Emit:Void(signalID:Int, data:Object = Null)
		_GetSignal(signalID).Emit(data)
	End Method

	Method Emit:Void(signalIDs:Int[], data:Object = Null)
		For Local id:Int = EachIn signalIDs
			_GetSignal(id).Emit(data)
		Next
	End Method

	Method EmitAll:Void(data:Object)
		For Local signal:FlxSignal = EachIn _signals.Values()
			signal.Emit(data)
		Next
	End Method

Private
	Method _GetSignal:FlxSignal(signalID:Int)
		Local s:FlxSignal = _signals.Get(signalID)

		If (s = Null) Then
			s = New FlxSignal()
			_signals.Set(signalID, s)
		End If

		Return s
	End Method

	Method _GetMethodInfo:MethodInfo(methodName:String, context:Object)
		Local classInfo:ClassInfo = GetClass(context)

		If (classInfo <> Null) Then
			Return classInfo.GetMethod(methodName,[])
		End If

		Return Null
	End Method

	Method _GetMethodSlot:FlxMethodSlot(methodInfo:MethodInfo, context:Object)
		If (methodInfo = Null) Return Null

		Local node:list.Node<FlxMethodSlot> = _methodSlots.FirstNode()

		While (node <> Null)
			If (node.Value()._method = methodInfo And node.Value()._context = context) Then
				Return node.Value()
			End If

			node = node.NextNode()
		Wend

		Return _methodSlots.AddLast(New FlxMethodSlot(context, methodInfo)).Value()
	End Method

	Method _GetFunctionSlot:FlxFunctionSlot(functionInfo:FunctionInfo)
		If (functionInfo = Null) Return Null

		Local node:list.Node<FlxFunctionSlot> = _functionSlots.FirstNode()

		While (node <> Null)
			If (node.Value()._function = functionInfo) Then
				Return node.Value()
			End If

			node = node.NextNode()
		Wend

		Return _functionSlots.AddLast(New FlxFunctionSlot(functionInfo)).Value()
	End Method

End Class

Private

Class FlxMethodSlot Implements FlxSignalListener

Private
	Field _context:Object

	Field _method:MethodInfo

Public
	Method New(context:Object, methodInfo:MethodInfo)
		_context = context
		_method = methodInfo
	End Method

	Method OnSignalEmitted:Void(signal:FlxSignal, data:Object)
		_method.Invoke(_context,[])
	End Method

End Class

Class FlxFunctionSlot Implements FlxSignalListener

Private
	Field _function:FunctionInfo

Public
	Method New(functionInfo:FunctionInfo)
		_function = functionInfo
	End Method

	Method OnSignalEmitted:Void(signal:FlxSignal, data:Object)
		_function.Invoke([])
	End Method

End Class

Strict

Import reflection
Import system.signals.flxsignal

Class FlxSignals

Private
	Field _signals:IntMap<FlxSignal>

	Field _methodSlots:List<FlxMethodSlot>

	Field _functionSlots:List<FlxFunctionSlot>

Public
	Method New()
		_signals = New IntMap<FlxSignal>()
		_methodSlots = New List<FlxMethodSlot>()
		_functionSlots = New List<FlxFunctionSlot>()
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener)
		_GetSignal(signalID).RegisterListener(listener)
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener, instant:Bool)
		_GetSignal(signalID).RegisterListener(listener, instant)
	End Method

	Method Connect:Void(signalID:Int, listener:FlxSignalListener, instant:Bool, priority:Int)
		_GetSignal(signalID).RegisterListener(listener, instant, priority)
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object)
		Connect(signalID, _GetMethodSlot(methodInfo, context))
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object, instant:Bool)
		Connect(signalID, _GetMethodSlot(methodInfo, context), instant)
	End Method

	Method Connect:Void(signalID:Int, methodInfo:MethodInfo, context:Object, instant:Bool, priority:Int)
		Connect(signalID, _GetMethodSlot(methodInfo, context), instant, priority)
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo)
		Connect(signalID, _GetFunctionSlot(functionInfo))
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo, instant:Bool)
		Connect(signalID, _GetFunctionSlot(functionInfo), instant)
	End Method

	Method Connect:Void(signalID:Int, functionInfo:FunctionInfo, instant:Bool, priority:Int)
		Connect(signalID, _GetFunctionSlot(functionInfo), instant, priority)
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context))
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object, instant:Bool)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context), instant)
	End Method

	Method ConnectM:Void(signalID:Int, methodName:String, context:Object, instant:Bool, priority:Int)
		Connect(signalID, _GetMethodSlot(_GetMethodInfo(methodName, context), context), instant, priority)
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])))
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String, instant:Bool)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])), instant)
	End Method

	Method ConnectF:Void(signalID:Int, functionName:String, instant:Bool, priority:Int)
		Connect(signalID, _GetFunctionSlot(GetFunction(functionName,[])), instant, priority)
	End Method

	Method Emit:Void(signalID:Int, data:Object = Null)
		_GetSignal(signalID).Emit(data)
	End Method

Private
	Method _GetSignal:FlxSignal(signalID:Int)
		Local s:FlxSignal = _signals.Get(signalID)

		If (s = Null) Then
			s = New FlxSignal()
			_signals.Set(signalID, s)
		End If

		Return s
	End Method

	Method _GetMethodInfo:MethodInfo(methodName:String, context:Object)
		Local classInfo:ClassInfo = GetClass(context)

		If (classInfo <> Null) Then
			Return classInfo.GetMethod(methodName,[])
		End If

		Return Null
	End Method

	Method _GetMethodSlot:FlxMethodSlot(methodInfo:MethodInfo, context:Object)
		If (methodInfo = Null) Return Null

		Local node:list.Node<FlxMethodSlot> = _methodSlots.FirstNode()

		While (node <> Null)
			If (node.Value()._method = methodInfo And node.Value()._context = context) Then
				Return node.Value()
			End If

			node = node.NextNode()
		Wend

		Return _methodSlots.AddLast(New FlxMethodSlot(context, methodInfo)).Value()
	End Method

	Method _GetFunctionSlot:FlxFunctionSlot(functionInfo:FunctionInfo)
		If (functionInfo = Null) Return Null

		Local node:list.Node<FlxFunctionSlot> = _functionSlots.FirstNode()

		While (node <> Null)
			If (node.Value()._function = functionInfo) Then
				Return node.Value()
			End If

			node = node.NextNode()
		Wend

		Return _functionSlots.AddLast(New FlxFunctionSlot(functionInfo)).Value()
	End Method

End Class

Private

Class FlxMethodSlot Implements FlxSignalListener

Private
	Field _context:Object

	Field _method:MethodInfo

Public
	Method New(context:Object, methodInfo:MethodInfo)
		_context = context
		_method = methodInfo
	End Method

	Method OnSignalEmitted:Void(signal:FlxSignal, data:Object)
		_method.Invoke(_context,[])
	End Method

End Class

Class FlxFunctionSlot Implements FlxSignalListener

Private
	Field _function:FunctionInfo

Public
	Method New(functionInfo:FunctionInfo)
		_function = functionInfo
	End Method

	Method OnSignalEmitted:Void(signal:FlxSignal, data:Object)
		_function.Invoke([])
	End Method

End Class