From 9dd1276aa19b9bb609199d903cbfdd0895b0b91a Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 28 Aug 2025 17:43:36 +0900 Subject: [PATCH] ... --- app/src/main/cpp/AnimationStrategy.cpp | 5 + app/src/main/cpp/AnimationStrategy.h | 30 ++ app/src/main/cpp/NoneAnimation.h | 17 + app/src/main/cpp/PageTurnAnimation.h | 35 ++ app/src/main/cpp/PanAnimation.h | 48 ++ app/src/main/cpp/PanOneWayAnimation.h | 53 +++ app/src/main/cpp/Renderer.cpp | 427 ++++++++++-------- app/src/main/cpp/Renderer.h | 86 ++-- app/src/main/cpp/TransitionStrategy.cpp | 160 +++++++ app/src/main/cpp/TransitionStrategy.h | 36 ++ app/src/main/cpp/ZoomAnimation.h | 33 ++ app/src/main/cpp/native_renderer.cpp | 7 + .../launcher/wall/MyWallpaperService.kt | 5 +- .../lunatic/launcher/wall/NativeRenderer.kt | 12 + 14 files changed, 730 insertions(+), 224 deletions(-) create mode 100644 app/src/main/cpp/AnimationStrategy.cpp create mode 100644 app/src/main/cpp/AnimationStrategy.h create mode 100644 app/src/main/cpp/NoneAnimation.h create mode 100644 app/src/main/cpp/PageTurnAnimation.h create mode 100644 app/src/main/cpp/PanAnimation.h create mode 100644 app/src/main/cpp/PanOneWayAnimation.h create mode 100644 app/src/main/cpp/TransitionStrategy.cpp create mode 100644 app/src/main/cpp/TransitionStrategy.h create mode 100644 app/src/main/cpp/ZoomAnimation.h diff --git a/app/src/main/cpp/AnimationStrategy.cpp b/app/src/main/cpp/AnimationStrategy.cpp new file mode 100644 index 00000000..dbe5355f --- /dev/null +++ b/app/src/main/cpp/AnimationStrategy.cpp @@ -0,0 +1,5 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// + +#include "AnimationStrategy.h" diff --git a/app/src/main/cpp/AnimationStrategy.h b/app/src/main/cpp/AnimationStrategy.h new file mode 100644 index 00000000..a237305a --- /dev/null +++ b/app/src/main/cpp/AnimationStrategy.h @@ -0,0 +1,30 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// +#pragma once +#include + +// 애니메이션의 현재 상태를 담을 구조체 +struct AnimationState { + float offsetX = 0.0f; + float offsetY = 0.0f; + float scale = 1.0f; + bool cycleComplete = false; +}; + +// 모든 '전문 요리사'의 기반이 될 추상 클래스 +class AnimationStrategy { +public: + virtual ~AnimationStrategy() = default; + + // 매 프레임마다 호출되어 애니메이션 상태를 업데이트하고 반환 + virtual AnimationState update(float overflowX, float overflowY) = 0; + + // 애니메이션 상태를 처음으로 리셋 + virtual void reset() = 0; + +protected: + // 생성자에서 애니메이션 속도를 받아 저장 + AnimationStrategy(float speed) : animationSpeed_(speed) {} + float animationSpeed_; +}; \ No newline at end of file diff --git a/app/src/main/cpp/NoneAnimation.h b/app/src/main/cpp/NoneAnimation.h new file mode 100644 index 00000000..00d4dcb5 --- /dev/null +++ b/app/src/main/cpp/NoneAnimation.h @@ -0,0 +1,17 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// +#include "AnimationStrategy.h" +#include // for std::max + +#ifndef LUNARLAUNCHER_NONEANIMATION_H +#define LUNARLAUNCHER_NONEANIMATION_H +class NoneAnimation : public AnimationStrategy { +public: + NoneAnimation(float speed) : AnimationStrategy(speed) { reset(); } + void reset() override { state_.cycleComplete = true; } + AnimationState update(float, float) override { return state_; } +private: + AnimationState state_; +}; +#endif //LUNARLAUNCHER_NONEANIMATION_H diff --git a/app/src/main/cpp/PageTurnAnimation.h b/app/src/main/cpp/PageTurnAnimation.h new file mode 100644 index 00000000..ff8bedd4 --- /dev/null +++ b/app/src/main/cpp/PageTurnAnimation.h @@ -0,0 +1,35 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// +#include "AnimationStrategy.h" +#include // for std::max + +#ifndef LUNARLAUNCHER_PAGETURNANIMATION_H +#define LUNARLAUNCHER_PAGETURNANIMATION_H + +// --- PAGE_TURN (대기 후 페이드) 애니메이션 --- +class PageTurnAnimation : public AnimationStrategy { +public: + PageTurnAnimation(float speed, long long delay) : AnimationStrategy(speed), delayMs_(delay) { reset(); } + + void reset() override { + state_.cycleComplete = false; + startTime_ = std::chrono::steady_clock::now(); + } + + AnimationState update(float, float) override { + if (state_.cycleComplete) return state_; + + long long elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime_).count(); + if (elapsed >= delayMs_) { + state_.cycleComplete = true; + } + return state_; + } +private: + AnimationState state_; + long long delayMs_; + std::chrono::steady_clock::time_point startTime_; +}; + +#endif //LUNARLAUNCHER_PAGETURNANIMATION_H diff --git a/app/src/main/cpp/PanAnimation.h b/app/src/main/cpp/PanAnimation.h new file mode 100644 index 00000000..bcf728bd --- /dev/null +++ b/app/src/main/cpp/PanAnimation.h @@ -0,0 +1,48 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// + +#ifndef LUNARLAUNCHER_PANANIMATION_H +#define LUNARLAUNCHER_PANANIMATION_H +#include "AnimationStrategy.h" +#include // for std::max + +// --- PAN (왕복) 애니메이션 --- +class PanAnimation : public AnimationStrategy { +public: + PanAnimation(float speed) : AnimationStrategy(speed) { reset(); } + + void reset() override { + state_.offsetX = 0.0f; + state_.offsetY = 0.0f; + state_.cycleComplete = false; + xDirection_ = 1; + yDirection_ = 1; + } + + AnimationState update(float overflowX, float overflowY) override { + if (state_.cycleComplete) return state_; + + bool xDone = (overflowX <= 0); + bool yDone = (overflowY <= 0); + + if (overflowX > 0) { + state_.offsetX += animationSpeed_ * xDirection_; + if (xDirection_ == 1 && state_.offsetX >= overflowX) { state_.offsetX = overflowX; xDirection_ = -1; } + else if (xDirection_ == -1 && state_.offsetX <= 0) { state_.offsetX = 0; xDirection_ = 1; xDone = true; } + } + if (overflowY > 0) { + state_.offsetY += animationSpeed_ * yDirection_; + if (yDirection_ == 1 && state_.offsetY >= overflowY) { state_.offsetY = overflowY; yDirection_ = -1; } + else if (yDirection_ == -1 && state_.offsetY <= 0) { state_.offsetY = 0; yDirection_ = 1; yDone = true; } + } + + if (xDone && yDone) state_.cycleComplete = true; + return state_; + } + +private: + AnimationState state_; + int xDirection_, yDirection_; +}; +#endif //LUNARLAUNCHER_PANANIMATION_H diff --git a/app/src/main/cpp/PanOneWayAnimation.h b/app/src/main/cpp/PanOneWayAnimation.h new file mode 100644 index 00000000..91182baa --- /dev/null +++ b/app/src/main/cpp/PanOneWayAnimation.h @@ -0,0 +1,53 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// +#include "AnimationStrategy.h" +#include // for std::max + +#ifndef LUNARLAUNCHER_PANONEWAYANIMATION_H +#define LUNARLAUNCHER_PANONEWAYANIMATION_H +// --- ⬇️ 새로운 PAN_ONE_WAY (편도) 애니메이션 클래스 추가 ⬇️ --- +class PanOneWayAnimation : public AnimationStrategy { +public: + PanOneWayAnimation(float speed) : AnimationStrategy(speed) { reset(); } + + void reset() override { + state_.offsetX = 0.0f; + state_.offsetY = 0.0f; + state_.cycleComplete = false; + } + + AnimationState update(float overflowX, float overflowY) override { + if (state_.cycleComplete) return state_; + + bool xReachedEnd = (overflowX <= 0); + bool yReachedEnd = (overflowY <= 0); + + if (overflowX > 0) { + state_.offsetX += animationSpeed_; // 항상 정방향(+)으로만 이동 + if (state_.offsetX >= overflowX) { + state_.offsetX = overflowX; // 끝에 도달하면 멈춤 + xReachedEnd = true; + } + } + + if (overflowY > 0) { + state_.offsetY += animationSpeed_; // 항상 정방향(+)으로만 이동 + if (state_.offsetY >= overflowY) { + state_.offsetY = overflowY; // 끝에 도달하면 멈춤 + yReachedEnd = true; + } + } + + // X축과 Y축 이동이 모두 끝났다면 사이클 완료 + if (xReachedEnd && yReachedEnd) { + state_.cycleComplete = true; + } + + return state_; + } + +private: + AnimationState state_; +}; +#endif //LUNARLAUNCHER_PANONEWAYANIMATION_H diff --git a/app/src/main/cpp/Renderer.cpp b/app/src/main/cpp/Renderer.cpp index ae222b35..1162c45c 100644 --- a/app/src/main/cpp/Renderer.cpp +++ b/app/src/main/cpp/Renderer.cpp @@ -1,95 +1,154 @@ #include "Renderer.h" +#include "AnimationStrategy.cpp" // 애니메이션 전략 클래스들의 구현을 포함 +#include "TransitionStrategy.cpp" // 전환 효과 전략 클래스들의 구현을 포함 +#include "PageTurnAnimation.h" +#include "ZoomAnimation.h" +#include "PanOneWayAnimation.h" +#include "PanAnimation.h" #include -#include +#include // for std::clamp, std::max #include #include -#include +#include // for std::chrono::high_resolution_clock +// JNI를 통해 Kotlin의 콜백 함수를 호출하기 위한 extern 선언 extern void callNextMediaCallback(); #define LOG_TAG "Renderer" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +/** + * @brief Renderer 생성자: 멤버 변수를 초기화합니다. + */ Renderer::Renderer() { + // 랜덤 엔진의 시드(seed)를 현재 시간으로 설정하여 매번 다른 랜덤 결과를 얻도록 합니다. randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + // 앱이 시작될 때 기본 애니메이션 및 전환 모드를 설정합니다. + setAnimationMode(static_cast(AnimationMode::PAN)); + setTransitionMode(static_cast(TransitionMode::FADE)); } +/** + * @brief Renderer 소멸자: release 함수를 호출하여 자원을 정리합니다. + */ Renderer::~Renderer() { release(); } +/** + * @brief 모든 미디어 에셋 관련 자원을 해제합니다. + */ void Renderer::release() { std::lock_guard lock(renderMutex_); currentMedia_.release(); nextMedia_.release(); } +/** + * @brief Preloader에게 다음 미디어를 미리 로드하라고 지시합니다. + * @param fd 로드할 파일의 파일 디스크립터 + */ void Renderer::setNextMedia(int fd) { preloader_.startNextPreload(fd); } +/** + * @brief 애니메이션 속도를 설정합니다. (Kotlin에서 호출) + * @param speed 프레임당 이동할 픽셀 수 또는 줌 속도 배율 + */ void Renderer::setAnimationSpeed(float speed) { - animationSpeed_ = speed; + animationSpeed_ = speed > 0 ? speed : 1.0f; } +/** + * @brief 페이드/전환 시간을 설정합니다. (Kotlin에서 호출) + * @param durationMs 밀리초 단위의 시간 + */ void Renderer::setFadeDuration(int durationMs) { fadeDurationMs_ = durationMs > 0 ? durationMs : 3000; } -// --- ⬇️ 새로운 setPageTurnDelay 함수 구현 ⬇️ --- +/** + * @brief PAGE_TURN 모드의 대기 시간을 설정합니다. (Kotlin에서 호출) + * @param delayMs 밀리초 단위의 시간 + */ void Renderer::setPageTurnDelay(int delayMs) { - pageTurnDelayMs_ = delayMs > 0 ? delayMs : 5000; // 0 이하면 기본값 5초 - LOGI("PageTurn delay set to %d ms", (int)pageTurnDelayMs_); + pageTurnDelayMs_ = delayMs > 0 ? delayMs : 5000; } +/** + * @brief 애니메이션 모드를 설정하고, 해당 모드에 맞는 전략 객체를 생성합니다. + * @param mode 설정할 애니메이션 모드 (int) + */ void Renderer::setAnimationMode(int mode) { configuredAnimationMode_ = (mode >= 0 && mode <= static_cast(AnimationMode::PAGE_TURN)) ? static_cast(mode) : AnimationMode::PAN; determineActiveAnimationMode(); - - animationCycleComplete_ = false; - currentOffsetX_ = 0.0f; - currentOffsetY_ = 0.0f; - currentZoom_ = 1.0f; - xDirection_ = 1; - yDirection_ = 1; - zoomDirection_ = 1; - // isTransitioning_ = false; // <-- 삭제 - isFading_ = false; - // transitionProgress_ = 0.0f; // <-- 삭제 - LOGI("Animation mode changed to %d", static_cast(configuredAnimationMode_)); } +/** + * @brief 전환 모드를 설정합니다. + * @param mode 설정할 전환 모드 (int) + */ +void Renderer::setTransitionMode(int mode) { + configuredTransitionMode_ = (mode >= 0 && mode <= static_cast(TransitionMode::MOSAIC)) + ? static_cast(mode) + : TransitionMode::FADE; +} + +/** + * @brief 설정된 모드가 RANDOM일 경우, 실제 적용할 애니메이션을 무작위로 결정합니다. + */ void Renderer::determineActiveAnimationMode() { + AnimationMode modeToSetActive; if (configuredAnimationMode_ == AnimationMode::RANDOM) { static const std::vector availableModes = { - AnimationMode::PAN, - AnimationMode::ZOOM, - AnimationMode::NONE, - AnimationMode::PAN_ONE_WAY, - AnimationMode::PAGE_TURN + AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE, + AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN }; std::uniform_int_distribution dist(0, availableModes.size() - 1); - activeAnimationMode_ = availableModes[dist(randomEngine_)]; - LOGI("Random mode active: Chose animation %d", static_cast(activeAnimationMode_)); + modeToSetActive = availableModes[dist(randomEngine_)]; + LOGI("Random mode active: Chose animation %d", static_cast(modeToSetActive)); } else { - activeAnimationMode_ = configuredAnimationMode_; + modeToSetActive = configuredAnimationMode_; + } + + activeAnimationMode_ = modeToSetActive; + // 결정된 모드에 맞는 '전문 요리사(전략 객체)'를 고용 + switch (activeAnimationMode_) { + case AnimationMode::PAN: + animationStrategy_ = std::make_unique(animationSpeed_); + break; + case AnimationMode::PAN_ONE_WAY: + animationStrategy_ = std::make_unique(animationSpeed_); + break; + case AnimationMode::ZOOM: + animationStrategy_ = std::make_unique(animationSpeed_); + break; + case AnimationMode::PAGE_TURN: + animationStrategy_ = std::make_unique(animationSpeed_, pageTurnDelayMs_); + break; + case AnimationMode::NONE: + default: + animationStrategy_ = std::make_unique(animationSpeed_); + break; } } +/** + * @brief 화면에 꽉 차도록 비율에 맞춰 스케일과 중앙 정렬 오프셋을 계산하는 헬퍼 함수 + */ void Renderer::calculateFitScaleAndOffset(const MediaAsset& media, int surfaceWidth, int surfaceHeight, float& outScale, float& outOffsetX, float& outOffsetY) const { - if (!media.isValid() || media.getWidth() == 0 || media.getHeight() == 0 || surfaceWidth == 0 || surfaceHeight == 0) { + if (!media.isValid() || media.getWidth() == 0 || media.getHeight() == 0) { outScale = 1.0f; outOffsetX = 0.0f; outOffsetY = 0.0f; return; } - float mediaAspect = static_cast(media.getWidth()) / media.getHeight(); float surfaceAspect = static_cast(surfaceWidth) / surfaceHeight; - if (mediaAspect > surfaceAspect) { outScale = static_cast(surfaceHeight) / media.getHeight(); outOffsetX = (static_cast(surfaceWidth) - (media.getWidth() * outScale)) / 2.0f; @@ -101,17 +160,18 @@ void Renderer::calculateFitScaleAndOffset(const MediaAsset& media, int surfaceWi } } + +/** + * @brief 현재 렌더러의 내부 상태를 문자열로 반환합니다. (디버깅용) + */ std::string Renderer::getDebugInfo() const { std::stringstream ss; ss << "========== Native State ==========\n"; - ss << " Configured Mode: " << static_cast(configuredAnimationMode_) << "\n"; - ss << " Active Mode : " << static_cast(activeAnimationMode_) << "\n"; - ss << " Fading: " << (isFading_ ? "YES" : "NO") << "\n"; - // ss << " Transitioning: " << (isTransitioning_ ? "YES" : "NO") << "\n"; // <-- 삭제 + ss << " Configured Anim: " << static_cast(configuredAnimationMode_) << "\n"; + ss << " Active Anim : " << static_cast(activeAnimationMode_) << "\n"; + ss << " Configured Trans: " << static_cast(configuredTransitionMode_) << "\n"; + ss << " In Transition: " << (isInTransition_ ? "YES" : "NO") << "\n"; ss << " Anim Complete: " << (animationCycleComplete_ ? "YES" : "NO") << "\n"; - ss << " Offset (X, Y): (" << std::fixed << std::setprecision(2) << currentOffsetX_ << ", " << currentOffsetY_ << ")\n"; - ss << " Zoom: " << std::fixed << std::setprecision(2) << currentZoom_ << "\n"; - ss << " PageTurnDelay: " << pageTurnDelayMs_ << " ms\n"; // <-- 추가 if (currentMedia_.isValid()) { ss << " Current Media: VALID [" @@ -128,40 +188,63 @@ std::string Renderer::getDebugInfo() const { } else { ss << " Next Media : INVALID"; } - return ss.str(); } +/** + * @brief 쇼를 총괄하는 무대 감독. 매 프레임마다 호출되어 화면에 그릴 모든 것을 결정하고 지시합니다. + * @param window 그림을 그릴 안드로이드 네이티브 윈도우(무대) + */ void Renderer::renderFrame(ANativeWindow* window) { + // ==================================================================== + // 1단계: 공연 준비 (기본 체크 및 무대 확보) + // ==================================================================== if (!window) return; - int surfaceWidth = ANativeWindow_getWidth(window); - int surfaceHeight = ANativeWindow_getHeight(window); std::lock_guard lock(renderMutex_); auto now = std::chrono::steady_clock::now(); + int surfaceWidth = ANativeWindow_getWidth(window); + int surfaceHeight = ANativeWindow_getHeight(window); - // 1. 현재 미디어 로딩 (초기 또는 페이드/전환 완료 후) + // ==================================================================== + // 2단계: 상태 업데이트 (그리기 전에 모든 상태를 최종 확정) + // ==================================================================== + + // -- 2A: 전환(Transition) 종료 처리 -- + if (isInTransition_) { + long long elapsed = std::chrono::duration_cast(now - transitionStartTime_).count(); + bool isComplete = false; + if (transitionStrategy_) isComplete = transitionStrategy_->isComplete(elapsed); + + if (isComplete) { + if (nextMedia_.isValid()) { + currentMedia_ = std::move(nextMedia_); + } + isInTransition_ = false; + animationCycleComplete_ = false; + determineActiveAnimationMode(); + if(animationStrategy_) animationStrategy_->reset(); + isFirstFrameForMedia_ = true; // 새 미디어의 첫 프레임임을 표시 + } + } + + // -- 2B: 미디어 준비 -- + // 현재 미디어가 없을 경우 Preloader에서 가져오기 if (!currentMedia_.isValid()) { if (preloader_.isPreloadedDataReady()) { currentMedia_ = preloader_.swapAndRelease(); if (currentMedia_.isValid()) { - LOGI("감독: 새 배우 등장 준비 완료."); - isFading_ = false; + LOGI("감독: 첫 배우 등장 준비 완료."); + isInTransition_ = false; determineActiveAnimationMode(); - animationCycleComplete_ = false; // 새로운 미디어는 애니메이션 사이클을 다시 시작 - currentOffsetX_ = 0.0f; currentOffsetY_ = 0.0f; currentZoom_ = 1.0f; - xDirection_ = 1; yDirection_ = 1; zoomDirection_ = 1; - - // PAGE_TURN 모드일 경우 대기 시간 시작 - if (activeAnimationMode_ == AnimationMode::PAGE_TURN) { - pageTurnStartTime_ = now; - LOGI("PAGE_TURN mode: Starting delay for %lld ms.", pageTurnDelayMs_); - } - callNextMediaCallback(); // 다음 미디어 미리 로드 요청 + if(animationStrategy_) animationStrategy_->reset(); + animationCycleComplete_ = false; + isFirstFrameForMedia_ = true; + callNextMediaCallback(); } } } - // 현재 미디어가 없으면 검은 화면만 출력 + // 그래도 무대에 세울 배우가 없다면 (아직 로딩 중) 검은 화면만 보여주고 퇴장 if (!currentMedia_.isValid()) { ANativeWindow_Buffer buffer; if (ANativeWindow_lock(window, &buffer, nullptr) == 0) { @@ -171,7 +254,7 @@ void Renderer::renderFrame(ANativeWindow* window) { return; } - // 2. 다음 미디어 미리 로딩 + // 다음 미디어가 없다면 Preloader에서 가져오기 if (!nextMedia_.isValid() && preloader_.isPreloadedDataReady()) { nextMedia_ = preloader_.swapAndRelease(); if(nextMedia_.isValid()) { @@ -180,145 +263,108 @@ void Renderer::renderFrame(ANativeWindow* window) { } } - // 3. 애니메이션 상태 업데이트 - // PAGE_TURN 모드의 대기 시간 처리 - if (activeAnimationMode_ == AnimationMode::PAGE_TURN && !isFading_) { - long long elapsedDelay = std::chrono::duration_cast(now - pageTurnStartTime_).count(); - if (elapsedDelay >= pageTurnDelayMs_) { - animationCycleComplete_ = true; // 대기 시간 경과, 페이드 시작 준비 - LOGI("PAGE_TURN mode: Delay complete, preparing for fade."); - } - } - - // 일반 애니메이션 (PAN, ZOOM 등) 업데이트 - if (!isFading_ && !animationCycleComplete_) { + // -- 2C: 애니메이션 진행 및 전환 시작 처리 -- + AnimationState animState; + if (!isInTransition_ && !animationCycleComplete_) { float overflowX = 0.0f, overflowY = 0.0f; - float mediaW = static_cast(currentMedia_.getWidth()); - float mediaH = static_cast(currentMedia_.getHeight()); - - // 화면 비율에 맞춰 확대되었을 때, 넘치는 부분 계산 - if ((mediaW / mediaH) > (static_cast(surfaceWidth) / surfaceHeight)) { - float scale = static_cast(surfaceHeight) / mediaH; - overflowX = std::max(0.0f, mediaW * scale - surfaceWidth); - } else { - float scale = static_cast(surfaceWidth) / mediaW; - overflowY = std::max(0.0f, mediaH * scale - surfaceHeight); - } - - switch (activeAnimationMode_) { - case AnimationMode::PAN: { - bool xCycleCompleted = (overflowX <= 0); - bool yCycleCompleted = (overflowY <= 0); - if (overflowX > 0) { - currentOffsetX_ += animationSpeed_ * xDirection_; - if (xDirection_ == 1 && currentOffsetX_ >= overflowX) { currentOffsetX_ = overflowX; xDirection_ = -1; } - else if (xDirection_ == -1 && currentOffsetX_ <= 0) { currentOffsetX_ = 0; xDirection_ = 1; xCycleCompleted = true; } - } - if (overflowY > 0) { - currentOffsetY_ += animationSpeed_ * yDirection_; - if (yDirection_ == 1 && currentOffsetY_ >= overflowY) { currentOffsetY_ = overflowY; yDirection_ = -1; } - else if (yDirection_ == -1 && currentOffsetY_ <= 0) { currentOffsetY_ = 0; yDirection_ = 1; yCycleCompleted = true; } - } - if (xCycleCompleted && yCycleCompleted) { animationCycleComplete_ = true; } - break; - } - case AnimationMode::PAN_ONE_WAY: { - bool xReachedEnd = (overflowX <= 0); - bool yReachedEnd = (overflowY <= 0); - if (overflowX > 0) { - currentOffsetX_ += animationSpeed_; - if (currentOffsetX_ >= overflowX) { currentOffsetX_ = overflowX; xReachedEnd = true; } - } - if (overflowY > 0) { - currentOffsetY_ += animationSpeed_; - if (currentOffsetY_ >= overflowY) { currentOffsetY_ = overflowY; yReachedEnd = true; } - } - if (xReachedEnd && yReachedEnd) { animationCycleComplete_ = true; } - break; - } - case AnimationMode::ZOOM: { - currentZoom_ += 0.0005f * animationSpeed_ * zoomDirection_; - if (zoomDirection_ == 1 && currentZoom_ >= 1.2f) { currentZoom_ = 1.2f; zoomDirection_ = -1; } - else if (zoomDirection_ == -1 && currentZoom_ <= 1.0f) { currentZoom_ = 1.0f; zoomDirection_ = 1; animationCycleComplete_ = true; } - break; - } - case AnimationMode::NONE: - case AnimationMode::PAGE_TURN: // PAGE_TURN은 대기 시간 로직에서 animationCycleComplete_를 설정 - default: { - animationCycleComplete_ = true; - break; - } - } - } - - // 4. 페이드 전환 처리 - if (animationCycleComplete_ && !isFading_ && nextMedia_.isValid()) { - isFading_ = true; - fadeStartTime_ = now; - LOGI("감독: 애니메이션 사이클 완료, 페이드 전환 시작."); - } - - float currentMediaAlpha = 1.0f; - float nextMediaAlpha = 0.0f; - - if (isFading_) { - long long elapsed = std::chrono::duration_cast(now - fadeStartTime_).count(); - currentMediaAlpha = std::clamp(1.0f - (float)elapsed / fadeDurationMs_, 0.0f, 1.0f); - nextMediaAlpha = std::clamp((float)elapsed / fadeDurationMs_, 0.0f, 1.0f); - - if (elapsed >= fadeDurationMs_) { // 페이드 전환 완료 시 - if (nextMedia_.isValid()) { - currentMedia_ = std::move(nextMedia_); // 다음 미디어를 현재 미디어로 교체 - determineActiveAnimationMode(); // 새 미디어에 적용할 애니메이션 모드 결정 - - animationCycleComplete_ = false; // 새 미디어는 애니메이션 사이클 다시 시작 - currentOffsetX_ = 0.0f; currentOffsetY_ = 0.0f; currentZoom_ = 1.0f; - xDirection_ = 1; yDirection_ = 1; zoomDirection_ = 1; - - // PAGE_TURN 모드일 경우 대기 시간 다시 시작 - if (activeAnimationMode_ == AnimationMode::PAGE_TURN) { - pageTurnStartTime_ = now; - LOGI("PAGE_TURN mode: Fade complete, starting new delay for %lld ms.", pageTurnDelayMs_); - } else { - LOGI("감독: 페이드 전환 완료, 새 배우 등장."); - } + // PAN 계열 애니메이션은 overflow 값(이미지가 화면보다 큰 정도)이 필요 + if (activeAnimationMode_ == AnimationMode::PAN || activeAnimationMode_ == AnimationMode::PAN_ONE_WAY) { + float mediaW = static_cast(currentMedia_.getWidth()); + float mediaH = static_cast(currentMedia_.getHeight()); + if ((mediaW / mediaH) > (static_cast(surfaceWidth) / surfaceHeight)) { + float scale = static_cast(surfaceHeight) / mediaH; + overflowX = std::max(0.0f, mediaW * scale - surfaceWidth); } else { - LOGW("감독: 페이드 완료되었으나 다음 배우가 준비되지 않음."); + float scale = static_cast(surfaceWidth) / mediaW; + overflowY = std::max(0.0f, mediaH * scale - surfaceHeight); } - isFading_ = false; } + + // 애니메이션 전문가에게 상태 계산을 맡김 + if (animationStrategy_) { + animState = animationStrategy_->update(overflowX, overflowY); + animationCycleComplete_ = animState.cycleComplete; + } + } else if (animationStrategy_) { + // 전환 중이거나 애니메이션이 끝났다면, 현재 상태를 그대로 유지 + animState = animationStrategy_->update(0,0); } - // 5. 화면 그리기 + // 애니메이션이 끝났고, 다음 미디어가 있고, 첫 프레임이 아니라면 -> 전환 시작! + if (animationCycleComplete_ && !isInTransition_ && nextMedia_.isValid() && !isFirstFrameForMedia_) { + isInTransition_ = true; + transitionStartTime_ = now; + + // 설정된 전환 모드를 확인 + TransitionMode transModeToUse = configuredTransitionMode_; + if (transModeToUse == TransitionMode::RANDOM) { + std::uniform_int_distribution dist(0, 1); // 0:FADE, 1:SLIDE + transModeToUse = static_cast(dist(randomEngine_)); + } + + // 모드에 맞는 전환 전문가 객체 생성 + if (transModeToUse == TransitionMode::SLIDE) { + transitionStrategy_ = std::make_unique(fadeDurationMs_, surfaceWidth); + } else if (transModeToUse == TransitionMode::FADE) { // 기본값 및 MOSAIC 대체는 FADE + transitionStrategy_ = std::make_unique(fadeDurationMs_); + }else if (transModeToUse == TransitionMode::MOSAIC) { // 기본값 및 MOSAIC 대체는 FADE + transitionStrategy_ = std::make_unique(fadeDurationMs_,30,30,randomEngine_); + } + if(transitionStrategy_) transitionStrategy_->reset(); + } + + // ==================================================================== + // 3단계: 그리기 (위에서 확정된 최종 상태를 기반으로 그림) + // ==================================================================== ANativeWindow_Buffer buffer; if (ANativeWindow_lock(window, &buffer, nullptr) != 0) return; - memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t)); // 무대를 검은색으로 지움 - // (A) 현재 배우 그리기 - drawMedia(buffer, currentMedia_, currentMediaAlpha, currentOffsetX_, currentOffsetY_, currentZoom_); + // -- 3A: 장면 전환(Transition)이 진행 중일 때 그리기 -- + if (isInTransition_ && transitionStrategy_ && nextMedia_.isValid()) { + long long elapsed = std::chrono::duration_cast(now - transitionStartTime_).count(); + // 전문가에게 무대(buffer)를 넘겨주고 모든 그리기를 위임 + transitionStrategy_->execute(this, buffer, currentMedia_, nextMedia_, elapsed); + } + // -- 3B: 일반 연기(Animation)가 진행 중일 때 그리기 -- + else { + memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t)); - // (B) 페이드 중이라면 다음 배우도 함께 그리기 - if (nextMedia_.isValid() && isFading_) { - drawMedia(buffer, nextMedia_, nextMediaAlpha, 0.0f, 0.0f, 1.0f); // 다음 배우는 애니메이션 없이 중앙 정렬 + float finalOffsetX, finalOffsetY, finalScale; + + // PAN 계열과 그 외 모드의 레이아웃 계산을 명확히 분리 + if (activeAnimationMode_ == AnimationMode::PAN || activeAnimationMode_ == AnimationMode::PAN_ONE_WAY) { + // --- ⬇️ PAN 모드일 때 이 로그가 찍혀야 합니다 ⬇️ --- + float scale; + if ((static_cast(currentMedia_.getWidth()) / currentMedia_.getHeight()) > (static_cast(surfaceWidth) / surfaceHeight)) { + scale = static_cast(surfaceHeight) / currentMedia_.getHeight(); + } else { + scale = static_cast(surfaceWidth) / currentMedia_.getWidth(); + } + finalScale = scale * animState.scale; + finalOffsetX = animState.offsetX; + finalOffsetY = animState.offsetY; + } else { + // --- ⬇️ 그 외 모드일 때 이 로그가 찍혀야 합니다 ⬇️ --- + float baseScale, baseOffsetX, baseOffsetY; + calculateFitScaleAndOffset(currentMedia_, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY); + finalScale = baseScale * animState.scale; + finalOffsetX = baseOffsetX + animState.offsetX; + finalOffsetY = baseOffsetY + animState.offsetY; + } + + drawMedia(buffer, currentMedia_, 1.0f, finalOffsetX, finalOffsetY, finalScale); } - ANativeWindow_unlockAndPost(window); // 화면 갱신 + ANativeWindow_unlockAndPost(window); + isFirstFrameForMedia_ = false; // 프레임 그리기가 끝났으므로 첫 프레임 플래그를 내림 } -void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float offsetX, float offsetY, float scaleMultiplier) { - // 1. 기본 체크: 배우가 무대에 설 수 있는 상태인지 확인. +/** + * @brief 지시를 수행하는 스태프. 이제 계산 없이 감독에게 받은 최종 값으로 그리기만 합니다. + */ +void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale) { if (!media.isValid() || alpha <= 0.0f) return; - // 2. 기본 크기/위치 계산: 배우를 무대에 꽉 차게 중앙 정렬하기 위한 기본값 계산. - float baseScale, baseOffsetX, baseOffsetY; - calculateFitScaleAndOffset(media, buffer.width, buffer.height, baseScale, baseOffsetX, baseOffsetY); - - // 3. 최종 크기/위치 계산: 감독의 지시(offsetX, offsetY, scaleMultiplier)를 기본값에 반영. - float finalScale = baseScale * scaleMultiplier; - float finalOffsetX = baseOffsetX + offsetX; - float finalOffsetY = baseOffsetY + offsetY; - - // 4. 최종 지시 전달: 계산된 최종 값으로 실제 그림 그리는 담당자에게 작업을 넘김. if (media.getType() == MediaAsset::Type::IMAGE) { renderImageFrame(media, buffer, finalScale, finalOffsetX, finalOffsetY, alpha); } else { @@ -326,6 +372,9 @@ void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float } } +/** + * @brief 비트맵 이미지의 한 프레임을 실제로 픽셀 단위로 그립니다. + */ void Renderer::renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) { uint32_t* dstPixels = (uint32_t*)buffer.bits; int dstStride = buffer.stride; @@ -337,30 +386,35 @@ void Renderer::renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& b uint8_t alphaByte = static_cast(alpha * 255.0f); for (int y = 0; y < buffer.height; ++y) { - int srcY = static_cast((y - offsetY) / scale); + // --- ⬇️ 이 부분이 최종 수정된 올바른 좌표 계산입니다 ⬇️ --- + int srcY = static_cast((y + offsetY) / scale); if (srcY < 0 || srcY >= imgH) continue; + uint32_t* dstRow = dstPixels + y * dstStride; for (int x = 0; x < buffer.width; ++x) { - int srcX = static_cast((x - offsetX) / scale); + // --- ⬇️ 이 부분이 최종 수정된 올바른 좌표 계산입니다 ⬇️ --- + int srcX = static_cast((x + offsetX) / scale); if (srcX < 0 || srcX >= imgW) continue; + const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4]; - // Dst 픽셀이 BGRA일 수 있으므로, RGB로 변환하여 알파 블렌딩 uint32_t dstPixelValue = dstRow[x]; - uint8_t dstB = (dstPixelValue >> 0) & 0xFF; // B component - uint8_t dstG = (dstPixelValue >> 8) & 0xFF; // G component - uint8_t dstR = (dstPixelValue >> 16) & 0xFF; // R component + uint8_t dstB = (dstPixelValue >> 0) & 0xFF; + uint8_t dstG = (dstPixelValue >> 8) & 0xFF; + uint8_t dstR = (dstPixelValue >> 16) & 0xFF; uint8_t finalR = (srcPixel[0] * alphaByte + dstR * (255 - alphaByte)) / 255; uint8_t finalG = (srcPixel[1] * alphaByte + dstG * (255 - alphaByte)) / 255; uint8_t finalB = (srcPixel[2] * alphaByte + dstB * (255 - alphaByte)) / 255; - // 출력 버퍼는 보통 BGRA (Android ARGB_8888은 BGRA 순서로 메모리에 저장됨) dstRow[x] = (0xFF << 24) | (finalR << 16) | (finalG << 8) | finalB; } } } +/** + * @brief 비디오의 한 프레임을 디코딩하고, 그 결과를 renderImageFrame을 통해 그립니다. + */ void Renderer::renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) { AVFormatContext* fmt_ctx = media.getFormatContext(); AVCodecContext* codec_ctx = media.getCodecContext(); @@ -384,10 +438,9 @@ void Renderer::renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, } } av_packet_unref(pkt); - } else if (ret == AVERROR_EOF) { // 비디오 끝에 도달하면 처음으로 되감기 + } else if (ret == AVERROR_EOF) { av_seek_frame(fmt_ctx, video_stream_idx, 0, AVSEEK_FLAG_BACKWARD); } - // 디코딩된 비디오 프레임은 이미지 데이터처럼 RGB 버퍼에 저장되어 있으므로 renderImageFrame을 사용 renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha); } \ No newline at end of file diff --git a/app/src/main/cpp/Renderer.h b/app/src/main/cpp/Renderer.h index 2537f6a9..38daeddc 100644 --- a/app/src/main/cpp/Renderer.h +++ b/app/src/main/cpp/Renderer.h @@ -1,6 +1,7 @@ #pragma once -#include "MediaAsset.h" +#include "AnimationStrategy.h" +#include "TransitionStrategy.h" #include "Preloader.h" #include #include @@ -8,11 +9,13 @@ #include #include #include +#include // for std::unique_ptr #include #include class Renderer { public: + // 사용자가 설정할 수 있는 애니메이션 모드 종류 enum class AnimationMode { PAN = 0, ZOOM = 1, @@ -22,55 +25,68 @@ public: PAGE_TURN = 5 }; + // 사용자가 설정할 수 있는 전환 효과 종류 + enum class TransitionMode { + FADE = 0, + SLIDE = 1, + RANDOM = 2, + MOSAIC = 3 + }; + Renderer(); ~Renderer(); + // --- Public API --- void setNextMedia(int fd); void renderFrame(ANativeWindow* window); void release(); std::string getDebugInfo() const; + // --- 설정(Settings)을 위한 함수들 --- void setAnimationSpeed(float speed); void setAnimationMode(int mode); void setFadeDuration(int durationMs); - void setPageTurnDelay(int delayMs); // <-- 새로운 setter 선언 + void setPageTurnDelay(int delayMs); + void setTransitionMode(int mode); -private: - std::mutex renderMutex_; - Preloader preloader_; - MediaAsset currentMedia_; - MediaAsset nextMedia_; - - AnimationMode configuredAnimationMode_ = AnimationMode::PAN; - AnimationMode activeAnimationMode_ = AnimationMode::PAN; - long long fadeDurationMs_ = 3000; - long long pageTurnDelayMs_ = 5000; // <-- PAGE_TURN 모드 대기 시간 (기본 5초) - float animationSpeed_ = 1.0f; - - // std::chrono::steady_clock::time_point transitionStartTime_; // <-- 삭제 - // bool isTransitioning_ = false; // <-- 삭제 - // float transitionProgress_ = 0.0f; // <-- 삭제 - - std::chrono::steady_clock::time_point fadeStartTime_; - std::chrono::steady_clock::time_point pageTurnStartTime_; // <-- PAGE_TURN 대기 시작 시간 추가 - bool isFading_ = false; - - float currentOffsetX_ = 0.0f; - float currentOffsetY_ = 0.0f; - int xDirection_ = 1; - int yDirection_ = 1; - - float currentZoom_ = 1.0f; - int zoomDirection_ = 1; - - bool animationCycleComplete_ = false; - - std::mt19937 randomEngine_; - - void determineActiveAnimationMode(); void calculateFitScaleAndOffset(const MediaAsset& media, int surfaceWidth, int surfaceHeight, float& outScale, float& outOffsetX, float& outOffsetY) const; +; + // --- 전략(Strategy) 객체들이 호출하는 헬퍼 함수들 --- + // (private에서 public으로 이동) void drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float offsetX, float offsetY, float scaleMultiplier); void renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha); void renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha); + +private: + std::mutex renderMutex_; + + // --- 멤버 객체들 --- + Preloader preloader_; + MediaAsset currentMedia_; + MediaAsset nextMedia_; + std::unique_ptr animationStrategy_; + std::unique_ptr transitionStrategy_; + + // --- 설정값 저장 변수 --- + AnimationMode configuredAnimationMode_ = AnimationMode::PAN; + TransitionMode configuredTransitionMode_ = TransitionMode::FADE; + AnimationMode activeAnimationMode_ = AnimationMode::PAN; + long long fadeDurationMs_ = 3000; + long long pageTurnDelayMs_ = 5000; + float animationSpeed_ = 1.0f; + + // --- 상태(State) 관리 변수 --- + bool isInTransition_ = false; + std::chrono::steady_clock::time_point transitionStartTime_; + + bool animationCycleComplete_ = false; + bool isFirstFrameForMedia_ = true; // 새 미디어의 첫 프레임인지 확인하는 플래그 + + // --- 랜덤 기능 --- + std::mt19937 randomEngine_; + + // --- private 헬퍼 함수 --- + void determineActiveAnimationMode(); + }; \ No newline at end of file diff --git a/app/src/main/cpp/TransitionStrategy.cpp b/app/src/main/cpp/TransitionStrategy.cpp new file mode 100644 index 00000000..0459d088 --- /dev/null +++ b/app/src/main/cpp/TransitionStrategy.cpp @@ -0,0 +1,160 @@ +#include "TransitionStrategy.h" +#include "Renderer.h" +#include // for std::clamp +#include +#include // for std::iota +#include // for std::shuffle + +// --- 페이드 전환 효과 --- +class FadeTransition : public TransitionStrategy { +public: + FadeTransition(long long duration) : TransitionStrategy(duration) {} + void reset() override {} + + bool isComplete(long long elapsedMs) const override { + return elapsedMs >= durationMs_; + } + + void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override { + float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); + + // 1. 사라지는 이미지(source)의 올바른 위치와 크기를 계산 + float srcScale, srcOffsetX, srcOffsetY; + renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcOffsetX, srcOffsetY); + + // 2. 나타나는 이미지(dest)의 올바른 위치와 크기를 계산 + float destScale, destOffsetX, destOffsetY; + renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destOffsetX, destOffsetY); + + // 3. 계산된 값으로 두 이미지를 그림 + renderer->drawMedia(buffer, source, 1.0f - progress, srcOffsetX, srcOffsetY, srcScale); + renderer->drawMedia(buffer, dest, progress, destOffsetX, destOffsetY, destScale); + + } +}; + +// --- 슬라이드 전환 효과 --- +class SlideTransition : public TransitionStrategy { +public: + SlideTransition(long long duration, int surfaceWidth) + : TransitionStrategy(duration), surfaceWidth_(surfaceWidth) {} + void reset() override {} + + bool isComplete(long long elapsedMs) const override { + return elapsedMs >= durationMs_; + } + + void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override { + float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); + float easeProgress = progress * progress * (3.0f - 2.0f * progress); + + // 1. 두 이미지의 기본 중앙 위치를 먼저 계산 + float srcScale, srcBaseOffsetX, srcBaseOffsetY; + renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcBaseOffsetX, srcBaseOffsetY); + + float destScale, destBaseOffsetX, destBaseOffsetY; + renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destBaseOffsetX, destBaseOffsetY); + + // 2. 슬라이드 효과를 위한 X축 이동거리 계산 + float sourceSlideX = -surfaceWidth_ * easeProgress; + float destSlideX = surfaceWidth_ * (1.0f - easeProgress); + + // 3. 기본 위치에 슬라이드 이동거리를 더해서 최종 위치 결정 + renderer->drawMedia(buffer, source, 1.0f, srcBaseOffsetX + sourceSlideX, srcBaseOffsetY, srcScale); + renderer->drawMedia(buffer, dest, 1.0f, destBaseOffsetX + destSlideX, destBaseOffsetY, destScale); + } +private: + int surfaceWidth_; +}; + + +// --- ⬇️ 모자이크 전환 효과 (전체 구현) ⬇️ --- +class MosaicTransition : public TransitionStrategy { +public: + MosaicTransition(long long duration, int tilesX, int tilesY, std::mt19937& randomEngine) + : TransitionStrategy(duration), tilesX_(tilesX), tilesY_(tilesY), randomEngine_(randomEngine) { + reset(); + } + + // 전환 시작 시 타일 순서를 섞음 + void reset() override { + int totalTiles = tilesX_ * tilesY_; + tileOrder_.resize(totalTiles); + std::iota(tileOrder_.begin(), tileOrder_.end(), 0); // 0, 1, 2, ... 순서로 벡터 채우기 + std::shuffle(tileOrder_.begin(), tileOrder_.end(), randomEngine_); // 순서 섞기 + + // 빠른 조회를 위한 역순 조회 테이블 생성 + revealOrder_.resize(totalTiles); + for(size_t i = 0; i < tileOrder_.size(); ++i) { + revealOrder_[tileOrder_[i]] = i; + } + } + + bool isComplete(long long elapsedMs) const override { + return elapsedMs >= durationMs_; + } + + // 모자이크 효과를 픽셀 단위로 직접 그림 + void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override { + float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); + int totalTiles = tilesX_ * tilesY_; + int tilesToReveal = static_cast(progress * totalTiles); + + // 두 이미지의 기본 스케일과 오프셋을 미리 계산 + float srcScale, srcOffsetX, srcOffsetY; + renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcOffsetX, srcOffsetY); + + float destScale, destOffsetX, destOffsetY; + renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destOffsetX, destOffsetY); + + const uint8_t* srcPixelData = source.getType() == MediaAsset::Type::IMAGE ? source.getImageData() : source.getRgbBuffer().data(); + const uint8_t* destPixelData = dest.getType() == MediaAsset::Type::IMAGE ? dest.getImageData() : dest.getRgbBuffer().data(); + + if (!srcPixelData || !destPixelData) return; + + uint32_t* dstPixels = (uint32_t*)buffer.bits; + int dstStride = buffer.stride; + float tileWidth = (float)buffer.width / tilesX_; + float tileHeight = (float)buffer.height / tilesY_; + + // 화면의 모든 픽셀을 순회 + for (int y = 0; y < buffer.height; ++y) { + uint32_t* dstRow = dstPixels + y * dstStride; + for (int x = 0; x < buffer.width; ++x) { + // 현재 픽셀이 속한 타일의 인덱스 계산 + int tileX = static_cast(x / tileWidth); + int tileY = static_cast(y / tileHeight); + int tileIndex = tileY * tilesX_ + tileX; + + // 이 타일이 몇 번째로 드러나야 하는지(rank) 확인 + int rank = revealOrder_[tileIndex]; + + const uint8_t* finalPixelData; + if (rank < tilesToReveal) { + // 드러나야 할 타일이면, 새 이미지(dest)에서 픽셀을 가져옴 + int srcX = static_cast((x + destOffsetX) / destScale); + int srcY = static_cast((y + destOffsetY) / destScale); + if (srcX >= 0 && srcX < dest.getWidth() && srcY >= 0 && srcY < dest.getHeight()) { + finalPixelData = &destPixelData[(srcY * dest.getWidth() + srcX) * 4]; + } else { continue; } + } else { + // 아직 드러나지 않은 타일이면, 이전 이미지(source)에서 픽셀을 가져옴 + int srcX = static_cast((x + srcOffsetX) / srcScale); + int srcY = static_cast((y + srcOffsetY) / srcScale); + if (srcX >= 0 && srcX < source.getWidth() && srcY >= 0 && srcY < source.getHeight()) { + finalPixelData = &srcPixelData[(srcY * source.getWidth() + srcX) * 4]; + } else { continue; } + } + + // 최종 픽셀을 버퍼에 씀 + dstRow[x] = (0xFF << 24) | (finalPixelData[0] << 16) | (finalPixelData[1] << 8) | finalPixelData[2]; + } + } + } + +private: + int tilesX_, tilesY_; + std::vector tileOrder_; + std::vector revealOrder_; // 역순 조회 테이블 + std::mt19937& randomEngine_; +}; \ No newline at end of file diff --git a/app/src/main/cpp/TransitionStrategy.h b/app/src/main/cpp/TransitionStrategy.h new file mode 100644 index 00000000..c596a6ac --- /dev/null +++ b/app/src/main/cpp/TransitionStrategy.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include "MediaAsset.h" + +// Renderer 클래스에 대한 전방 선언 (순환 참조 방지) +class Renderer; + +class TransitionStrategy { +public: + virtual ~TransitionStrategy() = default; + + // --- ⬇️ 두 함수로 역할을 분리 ⬇️ --- + /** + * @brief 전환이 완료되었는지 확인합니다. + * @param elapsedMs 전환 시작 후 경과 시간 + * @return 완료되었으면 true + */ + virtual bool isComplete(long long elapsedMs) const = 0; + + /** + * @brief 전환 효과를 직접 렌더링 버퍼에 그립니다. + * @param renderer Renderer 객체에 대한 포인터 (헬퍼 함수 호출용) + * @param buffer 그림을 그릴 캔버스 + * @param source 사라지는 미디어 + * @param dest 나타나는 미디어 + * @param elapsedMs 전환 시작 후 경과 시간 + * @return 전환이 완료되었으면 true, 아니면 false + */ + virtual void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) = 0; + + virtual void reset() = 0; + +protected: + TransitionStrategy(long long duration) : durationMs_(duration) {} + long long durationMs_; +}; \ No newline at end of file diff --git a/app/src/main/cpp/ZoomAnimation.h b/app/src/main/cpp/ZoomAnimation.h new file mode 100644 index 00000000..cfb2f448 --- /dev/null +++ b/app/src/main/cpp/ZoomAnimation.h @@ -0,0 +1,33 @@ +// +// Created by JIBUM HAN on 2025. 8. 28.. +// +#include "AnimationStrategy.h" +#include // for std::max +#ifndef LUNARLAUNCHER_ZOOMANIMATION_H +#define LUNARLAUNCHER_ZOOMANIMATION_H +// --- ZOOM 애니메이션 --- +class ZoomAnimation : public AnimationStrategy { +public: + ZoomAnimation(float speed) : AnimationStrategy(speed) { reset(); } + + void reset() override { + state_.scale = 1.0f; + state_.cycleComplete = false; + zoomDirection_ = 1; + } + + AnimationState update(float, float) override { + if (state_.cycleComplete) return state_; + + state_.scale += 0.0005f * animationSpeed_ * zoomDirection_; + if (zoomDirection_ == 1 && state_.scale >= 1.2f) { state_.scale = 1.2f; zoomDirection_ = -1; } + else if (zoomDirection_ == -1 && state_.scale <= 1.0f) { state_.scale = 1.0f; zoomDirection_ = 1; state_.cycleComplete = true; } + + return state_; + } + +private: + AnimationState state_; + int zoomDirection_; +}; +#endif //LUNARLAUNCHER_ZOOMANIMATION_H diff --git a/app/src/main/cpp/native_renderer.cpp b/app/src/main/cpp/native_renderer.cpp index cbe261f4..c515acdf 100644 --- a/app/src/main/cpp/native_renderer.cpp +++ b/app/src/main/cpp/native_renderer.cpp @@ -112,6 +112,13 @@ Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetAnimationMode(JNIEnv* en renderer->setAnimationMode(mode); } } +JNIEXPORT void JNICALL +Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetTransitionMode(JNIEnv* env, jobject, jlong nativeHandle, jint mode) { + Renderer* renderer = toNative(nativeHandle); + if (renderer) { + renderer->setTransitionMode(mode); + } +} JNIEXPORT void JNICALL Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetFadeDuration(JNIEnv* env, jobject, jlong nativeHandle, jint duration) { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index 6c21246e..be698a12 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -182,8 +182,9 @@ class MyWallpaperService : WallpaperService() { nativeRenderer = NativeRenderer() nativeRenderer?.initialize() // nativeInit() -> initialize() nativeRenderer?.setFadeDuration(1500) - nativeRenderer?.setTurnPageDuration(5000) - nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_PAGE_TURN) + nativeRenderer?.setTurnPageDuration(8000) + nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_RANDOM) + nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_RANDOM) nativeRenderer?.setAnimationSpeed(1.0f) // nativeSetAnimationSpeed -> setAnimationSpeed diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt index 0258ef95..cb8eda51 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt @@ -66,6 +66,12 @@ class NativeRenderer { } } + fun setTransitionMode(mode: Int) { + if (nativeHandle != 0L) { + nativeSetTransitionMode(nativeHandle, mode) + } + } + fun getDebugInfo(): String { return if (nativeHandle != 0L) { nativeGetDebugInfo(nativeHandle) @@ -85,6 +91,7 @@ class NativeRenderer { private external fun nativeSetFadeDuration(nativeHandle: Long, duration: Int) private external fun nativeGetDebugInfo(nativeHandle: Long): String private external fun nativeSetPageTurnDelay(nativeHandle: Long, duration: Int) + private external fun nativeSetTransitionMode(nativeHandle: Long, mode: Int) // --- Companion Object --- companion object { @@ -100,6 +107,11 @@ class NativeRenderer { const val ANIMATION_MODE_PAGE_TURN = 5 // <-- 새로운 상수 추가 + const val TRANSITION_MODE_FADE = 0 + const val TRANSITION_MODE_SLIDE = 1 + const val TRANSITION_MODE_RANDOM = 2 + const val TRANSITION_MODE_MOSAIC = 3 + // 이 콜백은 전역이라 일단 public external로 유지 @JvmStatic external fun nativeSetNextMediaCallback(callback: NextMediaCallback)