diff options
| author | Salanto <62221668+Salanto@users.noreply.github.com> | 2024-05-24 04:54:48 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-05-24 04:54:48 +0200 |
| commit | 4c56a25463d15cf12e21fe512a598bee91b3363d (patch) | |
| tree | b0478364cd4d267c97334164aa876b41c1a841f9 /src/animationlayer.cpp | |
| parent | 4b0f7e4d806c79313e493a3c58818e995af25847 (diff) | |
| parent | eb024cb93131cddba8ec1a6094abde8bf1f4eaf3 (diff) | |
Merge pull request #966 from AttorneyOnline/coolslide-rebased
[Feature] Courtroom slides + major AOLayer overhaul
Diffstat (limited to 'src/animationlayer.cpp')
| -rw-r--r-- | src/animationlayer.cpp | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp new file mode 100644 index 00000000..708aef26 --- /dev/null +++ b/src/animationlayer.cpp @@ -0,0 +1,702 @@ +#include "animationlayer.h" + +#include "aoapplication.h" +#include "options.h" + +#include <QRectF> +#include <QThreadPool> + +static QThreadPool *thread_pool; + +namespace kal +{ +AnimationLayer::AnimationLayer(QWidget *parent) + : QLabel(parent) +{ + setAlignment(Qt::AlignCenter); + + m_ticker = new QTimer(this); + m_ticker->setSingleShot(true); + m_ticker->setTimerType(Qt::PreciseTimer); + + connect(m_ticker, &QTimer::timeout, this, &AnimationLayer::frameTicker); + + if (!thread_pool) + { + thread_pool = new QThreadPool(qApp); + thread_pool->setMaxThreadCount(8); + } + + createLoader(); +} + +AnimationLayer::~AnimationLayer() +{ + deleteLoader(); +} + +QString AnimationLayer::fileName() +{ + return m_file_name; +} + +void AnimationLayer::setFileName(QString fileName) +{ + stopPlayback(); + m_file_name = fileName; + if (m_file_name.trimmed().isEmpty()) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::setFileName called with empty string"; +#endif + m_file_name = QObject::tr("Invalid File"); + } + resetData(); +} + +void AnimationLayer::startPlayback() +{ + if (m_processing) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::startPlayback called while already processing"; +#endif + return; + } + resetData(); + m_processing = true; + Q_EMIT startedPlayback(); + frameTicker(); +} + +void AnimationLayer::stopPlayback() +{ + if (m_ticker->isActive()) + { + m_ticker->stop(); + } + m_processing = false; + if (m_reset_cache_when_stopped) + { + createLoader(); + } + Q_EMIT stoppedPlayback(); +} + +void AnimationLayer::restartPlayback() +{ + stopPlayback(); + startPlayback(); +} + +void AnimationLayer::pausePlayback(bool enabled) +{ + if (m_pause == enabled) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::pausePlayback called with identical state"; +#endif + return; + } + m_pause = enabled; +} + +QSize AnimationLayer::frameSize() +{ + return m_frame_size; +} + +int AnimationLayer::frameCount() +{ + return m_frame_count; +} + +int AnimationLayer::currentFrameNumber() +{ + return m_frame_number; +} + +/** + * @brief AnimationLayer::jumpToFrame + * @param number The frame number to jump to. Must be in valid range. If the number is out of range, the method does nothing. + * @details If frame number is valid and playback is processing, the frame will immediately be displayed. + */ +void AnimationLayer::jumpToFrame(int number) +{ + if (number < 0 || number >= m_frame_count) + { +#ifdef DEBUG_MOVIE + qWarning() << "AnimationLayer::jumpToFrame failed to jump to frame" << number << "(file:" << m_file_name << ", frame count:" << m_frame_count << ")"; +#endif + return; + } + + bool is_processing = m_processing; + if (m_ticker->isActive()) + { + m_ticker->stop(); + } + m_target_frame_number = number; + if (is_processing) + { + frameTicker(); + } +} + +bool AnimationLayer::isPlayOnce() +{ + return m_play_once; +} + +void AnimationLayer::setPlayOnce(bool enabled) +{ + m_play_once = enabled; +} + +void AnimationLayer::setStretchToFit(bool enabled) +{ + m_stretch_to_fit = enabled; +} + +void AnimationLayer::setResetCacheWhenStopped(bool enabled) +{ + m_reset_cache_when_stopped = enabled; +} + +void AnimationLayer::setFlipped(bool enabled) +{ + m_flipped = enabled; +} + +void AnimationLayer::setTransformationMode(Qt::TransformationMode mode) +{ + m_transformation_mode_hint = mode; +} + +void AnimationLayer::setMinimumDurationPerFrame(int duration) +{ + m_minimum_duration = duration; +} + +void AnimationLayer::setMaximumDurationPerFrame(int duration) +{ + m_maximum_duration = duration; +} + +void AnimationLayer::setMaskingRect(QRect rect) +{ + m_mask_rect_hint = rect; + calculateFrameGeometry(); +} + +void AnimationLayer::resizeEvent(QResizeEvent *event) +{ + QLabel::resizeEvent(event); + calculateFrameGeometry(); +} + +void AnimationLayer::createLoader() +{ + deleteLoader(); + m_loader = new AnimationLoader(thread_pool); +} + +void AnimationLayer::deleteLoader() +{ + if (m_loader) + { + delete m_loader; + m_loader = nullptr; + } +} + +void AnimationLayer::resetData() +{ + m_first_frame = true; + m_frame_number = 0; + if (m_file_name != m_loader->loadedFileName()) + { + m_loader->load(m_file_name); + } + m_frame_count = m_loader->frameCount(); + m_frame_size = m_loader->size(); + m_frame_rect = QRect(QPoint(0, 0), m_frame_size); + m_ticker->stop(); + calculateFrameGeometry(); +} + +void AnimationLayer::calculateFrameGeometry() +{ + m_mask_rect = QRect(); + m_display_rect = QRect(); + m_scaled_frame_size = QSize(); + + QSize widget_size = size(); + if (!widget_size.isValid() || !m_frame_size.isValid()) + { + return; + } + + if (m_stretch_to_fit) + { + m_scaled_frame_size = widget_size; + } + else + { + QSize target_frame_size = m_frame_size; + if (m_frame_rect.contains(m_mask_rect_hint)) + { + m_mask_rect = m_mask_rect_hint; + target_frame_size = m_mask_rect_hint.size(); + } + + double scale = double(widget_size.height()) / double(target_frame_size.height()); + m_scaled_frame_size = target_frame_size * scale; + + // display the frame in its center + int x = (m_scaled_frame_size.width() - widget_size.width()) / 2; + m_display_rect = QRect(x, 0, widget_size.width(), m_scaled_frame_size.height()); + + if (m_transformation_mode_hint == Qt::FastTransformation) + { + m_transformation_mode = scale < 1.0 ? Qt::SmoothTransformation : Qt::FastTransformation; + } + } + + displayCurrentFrame(); +} + +void AnimationLayer::finishPlayback() +{ + stopPlayback(); + Q_EMIT finishedPlayback(); +} + +void AnimationLayer::prepareNextTick() +{ + int duration = qMax(m_minimum_duration, m_current_frame.duration); + duration = (m_maximum_duration > 0) ? qMin(m_maximum_duration, duration) : duration; + m_ticker->start(duration); +} + +void AnimationLayer::displayCurrentFrame() +{ + QPixmap image = m_current_frame.texture; + + if (m_frame_size.isValid()) + { + if (m_mask_rect.isValid()) + { + image = image.copy(m_mask_rect); + } + + if (!image.isNull()) + { + image = image.scaled(m_scaled_frame_size, Qt::IgnoreAspectRatio, m_transformation_mode); + + if (m_display_rect.isValid()) + { + image = image.copy(m_display_rect); + } + + if (m_flipped) + { + image = image.transformed(QTransform().scale(-1.0, 1.0)); + } + } + } + else + { + image = QPixmap(1, 1); + image.fill(Qt::transparent); + } + + setPixmap(image); +} + +void AnimationLayer::frameTicker() +{ + if (!m_processing) + { + return; + } + + if (m_frame_count < 1) + { + if (m_play_once) + { + finishPlayback(); + return; + } + + stopPlayback(); + return; + } + + if (m_pause && !m_first_frame) + { + return; + } + + if (m_frame_number == m_frame_count) + { + if (m_play_once) + { + finishPlayback(); + return; + } + + if (m_frame_count > 1) + { + m_frame_number = 0; + } + else + { + return; + } + } + + m_first_frame = false; + if (m_target_frame_number != -1) + { + m_frame_number = m_target_frame_number; + m_target_frame_number = -1; + } + m_current_frame = m_loader->frame(m_frame_number); + displayCurrentFrame(); + Q_EMIT frameNumberChanged(m_frame_number); + ++m_frame_number; + + if (!m_pause) + { + prepareNextTick(); + } +} + +CharacterAnimationLayer::CharacterAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + m_duration_timer = new QTimer(this); + m_duration_timer->setSingleShot(true); + connect(m_duration_timer, &QTimer::timeout, this, &CharacterAnimationLayer::onDurationLimitReached); + + connect(this, &CharacterAnimationLayer::stoppedPlayback, this, &CharacterAnimationLayer::onPlaybackStopped); + connect(this, &CharacterAnimationLayer::frameNumberChanged, this, &CharacterAnimationLayer::notifyFrameEffect); + connect(this, &CharacterAnimationLayer::finishedPlayback, this, &CharacterAnimationLayer::notifyEmotePlaybackFinished); +} + +void CharacterAnimationLayer::loadCharacterEmote(QString character, QString fileName, EmoteType emoteType, int durationLimit) +{ + auto is_dialog_emote = [](EmoteType emoteType) { + return emoteType == IdleEmote || emoteType == TalkEmote; + }; + + bool synchronize_frame = false; + const int previous_frame_count = frameCount(); + const int previous_frame_number = currentFrameNumber(); + if (m_character == character && m_emote == fileName && is_dialog_emote(m_emote_type) && is_dialog_emote(emoteType)) + { + synchronize_frame = true; + } + + m_character = character; + m_emote = fileName; + m_resolved_emote = fileName; + m_emote_type = emoteType; + + QStringList prefixes; + bool placeholder_fallback = false; + bool play_once = false; + switch (emoteType) + { + default: + break; + + case PreEmote: + play_once = true; + break; + + case IdleEmote: + prefixes << QStringLiteral("(a)") << QStringLiteral("(a)/"); + placeholder_fallback = true; + break; + + case TalkEmote: + prefixes << QStringLiteral("(b)") << QStringLiteral("(b)/"); + placeholder_fallback = true; + break; + + case PostEmote: + prefixes << QStringLiteral("(c)") << QStringLiteral("(c)/"); + break; + } + + QVector<VPath> path_list; + QVector<QString> prefixed_emote_list; + for (const QString &prefix : qAsConst(prefixes)) + { + path_list << ao_app->get_character_path(character, prefix + m_emote); + prefixed_emote_list << prefix + m_emote; + } + path_list << ao_app->get_character_path(character, m_emote); + prefixed_emote_list << m_emote; + + if (placeholder_fallback) + { + path_list << ao_app->get_character_path(character, QStringLiteral("placeholder")); + prefixed_emote_list << QStringLiteral("placeholder"); + path_list << ao_app->get_theme_path("placeholder", ao_app->default_theme); + prefixed_emote_list << QStringLiteral("placeholder"); + } + + int index = -1; + QString file_path = ao_app->get_image_path(path_list, index); + if (index != -1) + { + m_resolved_emote = prefixed_emote_list[index]; + } + + setFileName(file_path); + setPlayOnce(play_once); + setTransformationMode(ao_app->get_scaling(ao_app->get_emote_property(character, fileName, "scaling"))); + setStretchToFit(ao_app->get_emote_property(character, fileName, "stretch").startsWith("true")); + if (synchronize_frame && previous_frame_count == frameCount()) + { + jumpToFrame(previous_frame_number); + } + m_duration = durationLimit; +} + +void CharacterAnimationLayer::setFrameEffects(QStringList data) +{ + m_effects.clear(); + + static const QList<EffectType> EFFECT_TYPE_LIST{ShakeEffect, FlashEffect, SfxEffect}; + for (int i = 0; i < data.length(); ++i) + { + const EffectType effect_type = EFFECT_TYPE_LIST.at(i); + + QStringList emotes = data.at(i).split("^"); + for (const QString &emote : qAsConst(emotes)) + { + QStringList emote_effects = emote.split("|"); + + const QString emote_name = emote_effects.takeFirst(); + + for (const QString &raw_effect : qAsConst(emote_effects)) + { + QStringList frame_data = raw_effect.split("="); + + const int frame_number = frame_data.at(0).toInt(); + + FrameEffect effect; + effect.emote_name = emote_name; + effect.type = effect_type; + if (effect_type == EffectType::SfxEffect) + { + effect.file_name = frame_data.at(1); + } + + m_effects[frame_number].append(effect); + } + } + } +} + +void CharacterAnimationLayer::startTimeLimit() +{ + if (m_duration > 0) + { + m_duration_timer->start(m_duration); + } +} + +void CharacterAnimationLayer::onPlaybackStopped() +{ + if (m_duration_timer->isActive()) + { + m_duration_timer->stop(); + } +} + +void CharacterAnimationLayer::notifyEmotePlaybackFinished() +{ + if (m_emote_type == PreEmote || m_emote_type == PostEmote) + { + Q_EMIT finishedPreOrPostEmotePlayback(); + } +} + +void CharacterAnimationLayer::onPlaybackFinished() +{ + if (m_emote_type == PreEmote || m_emote_type == PostEmote) + { + if (m_duration_timer->isActive()) + { + m_duration_timer->stop(); + } + + notifyEmotePlaybackFinished(); + } +} + +void CharacterAnimationLayer::onDurationLimitReached() +{ + stopPlayback(); + notifyEmotePlaybackFinished(); +} + +void CharacterAnimationLayer::notifyFrameEffect(int frameNumber) +{ + auto it = m_effects.constFind(frameNumber); + if (it != m_effects.constEnd()) + { + for (const FrameEffect &effect : qAsConst(*it)) + { + if (effect.emote_name == m_resolved_emote) + { + switch (effect.type) + { + default: + break; + + case EffectType::SfxEffect: + Q_EMIT soundEffect(effect.file_name); + break; + + case EffectType::ShakeEffect: + Q_EMIT shakeEffect(); + break; + + case EffectType::FlashEffect: + Q_EMIT flashEffect(); + break; + } + } + } + } +} + +BackgroundAnimationLayer::BackgroundAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{} + +void BackgroundAnimationLayer::loadAndPlayAnimation(QString fileName) +{ + QString file_path = ao_app->get_image_suffix(ao_app->get_background_path(fileName)); +#ifdef DEBUG_MOVIE + if (file_path.isEmpty()) + { + qWarning() << "[BackgroundLayer] Failed to load background:" << fileName; + } + else if (file_path == this->fileName()) + { + return; + } + else + { + qInfo() << "[BackgroundLayer] Loading background:" << file_path; + } +#endif + + bool is_different_file = file_path != this->fileName(); + if (is_different_file) + { + setFileName(file_path); + } + + VPath design_path = ao_app->get_background_path("design.ini"); + setTransformationMode(ao_app->get_scaling(ao_app->read_design_ini("scaling", design_path))); + setStretchToFit(ao_app->read_design_ini("stretch", design_path).startsWith("true")); + + if (is_different_file) + { + startPlayback(); + } +} + +SplashAnimationLayer::SplashAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &SplashAnimationLayer::startedPlayback, this, &SplashAnimationLayer::show); + connect(this, &SplashAnimationLayer::stoppedPlayback, this, &SplashAnimationLayer::hide); +} + +void SplashAnimationLayer::loadAndPlayAnimation(QString p_filename, QString p_charname, QString p_miscname) +{ + QString file_path = ao_app->get_image(p_filename, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, p_miscname, p_charname, "placeholder"); + setFileName(file_path); + setTransformationMode(ao_app->get_misc_scaling(p_miscname)); + startPlayback(); +} + +EffectAnimationLayer::EffectAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &EffectAnimationLayer::startedPlayback, this, &EffectAnimationLayer::show); + connect(this, &EffectAnimationLayer::stoppedPlayback, this, &EffectAnimationLayer::maybeHide); +} + +void EffectAnimationLayer::loadAndPlayAnimation(QString p_filename, bool repeat) +{ + setFileName(p_filename); + setPlayOnce(!repeat); + startPlayback(); +} + +void EffectAnimationLayer::setHideWhenStopped(bool enabled) +{ + m_hide_when_stopped = enabled; +} + +void EffectAnimationLayer::maybeHide() +{ + if (m_hide_when_stopped && isPlayOnce()) + { + hide(); + } +} + +InterfaceAnimationLayer::InterfaceAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + setStretchToFit(true); + + connect(this, &InterfaceAnimationLayer::startedPlayback, this, &InterfaceAnimationLayer::show); + connect(this, &InterfaceAnimationLayer::stoppedPlayback, this, &InterfaceAnimationLayer::hide); +} + +void InterfaceAnimationLayer::loadAndPlayAnimation(QString fileName, QString miscName) +{ + QString file_path = ao_app->get_image(fileName, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, miscName); + setFileName(file_path); + startPlayback(); +} + +StickerAnimationLayer::StickerAnimationLayer(AOApplication *ao_app, QWidget *parent) + : AnimationLayer(parent) + , ao_app(ao_app) +{ + connect(this, &StickerAnimationLayer::startedPlayback, this, &StickerAnimationLayer::show); + connect(this, &StickerAnimationLayer::stoppedPlayback, this, &StickerAnimationLayer::hide); +} + +void StickerAnimationLayer::loadAndPlayAnimation(QString fileName) +{ + QString misc_file; // FIXME this is a bad name + if (Options::getInstance().customChatboxEnabled()) + { + misc_file = ao_app->get_chat(fileName); + } + + QString file_path = ao_app->get_image("sticker/" + fileName, Options::getInstance().theme(), Options::getInstance().subTheme(), ao_app->default_theme, misc_file); + setFileName(file_path); + setTransformationMode(ao_app->get_misc_scaling(misc_file)); + startPlayback(); +} +} // namespace kal |
