mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
All viewer animation controls are now ported.
This commit is contained in:
parent
2ab0baa3b5
commit
874cff7ae5
@ -8,7 +8,8 @@ import world.phantasmal.lib.fileFormats.ninja.NjKeyframeTrack
|
||||
import world.phantasmal.lib.fileFormats.ninja.NjMotion
|
||||
import world.phantasmal.web.externals.three.*
|
||||
|
||||
const val PSO_FRAME_RATE: Double = 30.0
|
||||
const val PSO_FRAME_RATE: Int = 30
|
||||
const val PSO_FRAME_RATE_DOUBLE: Double = PSO_FRAME_RATE.toDouble()
|
||||
|
||||
fun createAnimationClip(njObject: NinjaObject<*>, njMotion: NjMotion): AnimationClip {
|
||||
val interpolation =
|
||||
@ -27,7 +28,7 @@ fun createAnimationClip(njObject: NinjaObject<*>, njMotion: NjMotion): Animation
|
||||
val times = jsArrayOf<Double>()
|
||||
|
||||
for (keyframe in track.keyframes) {
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE)
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE_DOUBLE)
|
||||
}
|
||||
|
||||
val values = jsArrayOf<Double>()
|
||||
@ -114,7 +115,7 @@ fun createAnimationClip(njObject: NinjaObject<*>, njMotion: NjMotion): Animation
|
||||
|
||||
return AnimationClip(
|
||||
"Animation",
|
||||
(njMotion.frameCount - 1) / PSO_FRAME_RATE,
|
||||
(njMotion.frameCount - 1) / PSO_FRAME_RATE_DOUBLE,
|
||||
tracks.asArray(),
|
||||
).optimize()
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
|
||||
val showSkeleton: Val<Boolean> = store.showSkeleton
|
||||
val playAnimation: Val<Boolean> = store.animationPlaying
|
||||
val clearCurrentAnimationButtonEnabled = store.currentNinjaMotion.isNotNull()
|
||||
val frameRate: Val<Int> = store.frameRate
|
||||
val frame: Val<Int> = store.frame
|
||||
val animationControlsEnabled: Val<Boolean> = store.currentNinjaMotion.isNotNull()
|
||||
val maxFrame: Val<String> = store.currentNinjaMotion.map { "/ ${it?.frameCount ?: 0}" }
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
val resultMessage: Val<String> = result.map {
|
||||
@ -42,6 +45,14 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
store.setAnimationPlaying(play)
|
||||
}
|
||||
|
||||
fun setFrameRate(frameRate: Int) {
|
||||
store.setFrameRate(frameRate)
|
||||
}
|
||||
|
||||
fun setFrame(frame: Int) {
|
||||
store.setFrame(frame)
|
||||
}
|
||||
|
||||
suspend fun clearCurrentAnimation() {
|
||||
store.setCurrentAnimation(null)
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ import world.phantasmal.core.math.degToRad
|
||||
import world.phantasmal.lib.fileFormats.ninja.NjMotion
|
||||
import world.phantasmal.web.core.rendering.*
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.rendering.conversion.PSO_FRAME_RATE_DOUBLE
|
||||
import world.phantasmal.web.core.rendering.conversion.createAnimationClip
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToSkinnedMesh
|
||||
import world.phantasmal.web.core.times
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.web.viewer.stores.ViewerStore
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.tan
|
||||
|
||||
class MeshRenderer(
|
||||
@ -21,6 +23,7 @@ class MeshRenderer(
|
||||
private var mesh: Mesh? = null
|
||||
private var skeletonHelper: SkeletonHelper? = null
|
||||
private var animation: Animation? = null
|
||||
private var updateAnimationTime = true
|
||||
private var charClassActive = false
|
||||
|
||||
override val context = addDisposable(RenderContext(
|
||||
@ -48,6 +51,8 @@ class MeshRenderer(
|
||||
observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged)
|
||||
observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it }
|
||||
observe(viewerStore.animationPlaying, ::animationPlayingChanged)
|
||||
observe(viewerStore.frameRate, ::frameRateChanged)
|
||||
observe(viewerStore.frame, ::frameChanged)
|
||||
}
|
||||
|
||||
override fun render() {
|
||||
@ -58,11 +63,13 @@ class MeshRenderer(
|
||||
super.render()
|
||||
|
||||
animation?.let {
|
||||
// TODO: Update current animation frame in store.
|
||||
// val action = it.mixer.clipAction(it.clip)
|
||||
//
|
||||
// if (!action.paused) {
|
||||
// }
|
||||
val action = it.mixer.clipAction(it.clip)
|
||||
|
||||
if (!action.paused) {
|
||||
updateAnimationTime = false
|
||||
viewerStore.setFrame((action.time * PSO_FRAME_RATE_DOUBLE + 1).roundToInt())
|
||||
updateAnimationTime = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,8 +133,7 @@ class MeshRenderer(
|
||||
// Create a new animation mixer and clip.
|
||||
viewerStore.currentNinjaMotion.value?.let { nj_motion ->
|
||||
val mixer = AnimationMixer(mesh)
|
||||
// TODO: Set time scale.
|
||||
// mixer.timeScale = this.store.animation_frame_rate.val / PSO_FRAME_RATE;
|
||||
mixer.timeScale = viewerStore.frameRate.value / PSO_FRAME_RATE_DOUBLE
|
||||
|
||||
val clip = createAnimationClip(ninjaObject, nj_motion)
|
||||
|
||||
@ -170,8 +176,8 @@ class MeshRenderer(
|
||||
}
|
||||
|
||||
private fun animationPlayingChanged(playing: Boolean) {
|
||||
animation?.let { animation ->
|
||||
animation.mixer.clipAction(animation.clip).paused = !playing
|
||||
animation?.let {
|
||||
it.mixer.clipAction(it.clip).paused = !playing
|
||||
|
||||
if (playing) {
|
||||
clock.start()
|
||||
@ -181,6 +187,20 @@ class MeshRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun frameRateChanged(frameRate: Int) {
|
||||
animation?.let {
|
||||
it.mixer.timeScale = frameRate / PSO_FRAME_RATE_DOUBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun frameChanged(frame: Int) {
|
||||
if (updateAnimationTime) {
|
||||
animation?.let {
|
||||
it.mixer.clipAction(it.clip).time = (frame - 1) / PSO_FRAME_RATE_DOUBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Animation(val mixer: AnimationMixer, val clip: AnimationClip)
|
||||
|
||||
companion object {
|
||||
|
@ -11,6 +11,7 @@ import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.rendering.conversion.PSO_FRAME_RATE
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.web.viewer.ViewerUrls
|
||||
@ -42,6 +43,8 @@ class ViewerStore(
|
||||
// Settings.
|
||||
private val _showSkeleton = mutableVal(false)
|
||||
private val _animationPlaying = mutableVal(true)
|
||||
private val _frameRate = mutableVal(PSO_FRAME_RATE)
|
||||
private val _frame = mutableVal(0)
|
||||
|
||||
// Ninja concepts.
|
||||
val currentNinjaObject: Val<NinjaObject<*>?> = _currentNinjaObject
|
||||
@ -63,6 +66,8 @@ class ViewerStore(
|
||||
// Settings.
|
||||
val showSkeleton: Val<Boolean> = _showSkeleton
|
||||
val animationPlaying: Val<Boolean> = _animationPlaying
|
||||
val frameRate: Val<Int> = _frameRate
|
||||
val frame: Val<Int> = _frame
|
||||
|
||||
init {
|
||||
for (path in listOf(ViewerUrls.mesh, ViewerUrls.texture)) {
|
||||
@ -198,6 +203,20 @@ class ViewerStore(
|
||||
_animationPlaying.value = playing
|
||||
}
|
||||
|
||||
fun setFrameRate(frameRate: Int) {
|
||||
_frameRate.value = frameRate
|
||||
}
|
||||
|
||||
fun setFrame(frame: Int) {
|
||||
val maxFrame = currentNinjaMotion.value?.frameCount ?: Int.MAX_VALUE
|
||||
|
||||
_frame.value = when {
|
||||
frame > maxFrame -> 1
|
||||
frame < 1 -> maxFrame
|
||||
else -> frame
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadCharacterClassNinjaObject(clearAnimation: Boolean) {
|
||||
val char = currentCharacterClass.value
|
||||
?: return
|
||||
|
@ -28,12 +28,34 @@ class ViewerToolbar(private val ctrl: ViewerToolbarController) : Widget() {
|
||||
),
|
||||
Checkbox(
|
||||
label = "Play animation",
|
||||
enabled = ctrl.animationControlsEnabled,
|
||||
checked = ctrl.playAnimation,
|
||||
onChange = ctrl::setPlayAnimation,
|
||||
),
|
||||
IntInput(
|
||||
label = "Frame rate:",
|
||||
enabled = ctrl.animationControlsEnabled,
|
||||
value = ctrl.frameRate,
|
||||
onChange = ctrl::setFrameRate,
|
||||
min = 1,
|
||||
max = 240,
|
||||
step = 1,
|
||||
),
|
||||
IntInput(
|
||||
label = "Frame:",
|
||||
enabled = ctrl.animationControlsEnabled,
|
||||
value = ctrl.frame,
|
||||
onChange = ctrl::setFrame,
|
||||
step = 1,
|
||||
),
|
||||
Label(
|
||||
enabled = ctrl.animationControlsEnabled,
|
||||
textVal = ctrl.maxFrame,
|
||||
),
|
||||
Button(
|
||||
className = "pw-viewer-toolbar-clear-animation",
|
||||
text = "Clear animation",
|
||||
enabled = ctrl.clearCurrentAnimationButtonEnabled,
|
||||
enabled = ctrl.animationControlsEnabled,
|
||||
onClick = { scope.launch { ctrl.clearCurrentAnimation() } },
|
||||
),
|
||||
)
|
||||
@ -45,4 +67,16 @@ class ViewerToolbar(private val ctrl: ViewerToolbarController) : Widget() {
|
||||
onDismiss = ctrl::dismissResultDialog,
|
||||
))
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
style("""
|
||||
.pw-viewer-toolbar > .pw-toolbar > .pw-viewer-toolbar-clear-animation {
|
||||
margin-left: 6px;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ open class Button(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
enabled: Val<Boolean> = trueVal(),
|
||||
tooltip: Val<String?> = nullVal(),
|
||||
private val className: String? = null,
|
||||
private val text: String? = null,
|
||||
private val textVal: Val<String>? = null,
|
||||
private val iconLeft: Icon? = null,
|
||||
@ -29,6 +30,9 @@ open class Button(
|
||||
override fun Node.createElement() =
|
||||
button {
|
||||
className = "pw-button"
|
||||
|
||||
this@Button.className?.let { classList.add(it) }
|
||||
|
||||
onmousedown = onMouseDown
|
||||
onmouseup = onMouseUp
|
||||
onclick = onClick
|
||||
|
@ -13,6 +13,7 @@ class FileButton(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
enabled: Val<Boolean> = trueVal(),
|
||||
tooltip: Val<String?> = nullVal(),
|
||||
className: String? = null,
|
||||
text: String? = null,
|
||||
textVal: Val<String>? = null,
|
||||
iconLeft: Icon? = null,
|
||||
@ -20,7 +21,7 @@ class FileButton(
|
||||
private val accept: String = "",
|
||||
private val multiple: Boolean = false,
|
||||
private val filesSelected: ((List<File>) -> Unit)? = null,
|
||||
) : Button(visible, enabled, tooltip, text, textVal, iconLeft, iconRight) {
|
||||
) : Button(visible, enabled, tooltip, className, text, textVal, iconLeft, iconRight) {
|
||||
override fun interceptElement(element: HTMLElement) {
|
||||
element.classList.add("pw-file-button")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user