This commit is contained in:
lunaticbum 2025-08-28 17:43:36 +09:00
parent 6404f4176e
commit 9dd1276aa1
14 changed files with 730 additions and 224 deletions

View File

@ -0,0 +1,5 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#include "AnimationStrategy.h"

View File

@ -0,0 +1,30 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#pragma once
#include <chrono>
// 애니메이션의 현재 상태를 담을 구조체
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_;
};

View File

@ -0,0 +1,17 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#include "AnimationStrategy.h"
#include <algorithm> // 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

View File

@ -0,0 +1,35 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#include "AnimationStrategy.h"
#include <algorithm> // 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::milliseconds>(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

View File

@ -0,0 +1,48 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#ifndef LUNARLAUNCHER_PANANIMATION_H
#define LUNARLAUNCHER_PANANIMATION_H
#include "AnimationStrategy.h"
#include <algorithm> // 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

View File

@ -0,0 +1,53 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#include "AnimationStrategy.h"
#include <algorithm> // 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

View File

@ -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 <android/log.h>
#include <algorithm>
#include <algorithm> // for std::clamp, std::max
#include <cmath>
#include <thread>
#include <chrono>
#include <chrono> // 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<int>(AnimationMode::PAN));
setTransitionMode(static_cast<int>(TransitionMode::FADE));
}
/**
* @brief Renderer : release .
*/
Renderer::~Renderer() {
release();
}
/**
* @brief .
*/
void Renderer::release() {
std::lock_guard<std::mutex> 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<int>(AnimationMode::PAGE_TURN))
? static_cast<AnimationMode>(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<int>(configuredAnimationMode_));
}
/**
* @brief .
* @param mode (int)
*/
void Renderer::setTransitionMode(int mode) {
configuredTransitionMode_ = (mode >= 0 && mode <= static_cast<int>(TransitionMode::MOSAIC))
? static_cast<TransitionMode>(mode)
: TransitionMode::FADE;
}
/**
* @brief RANDOM일 , .
*/
void Renderer::determineActiveAnimationMode() {
AnimationMode modeToSetActive;
if (configuredAnimationMode_ == AnimationMode::RANDOM) {
static const std::vector<AnimationMode> 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<size_t> dist(0, availableModes.size() - 1);
activeAnimationMode_ = availableModes[dist(randomEngine_)];
LOGI("Random mode active: Chose animation %d", static_cast<int>(activeAnimationMode_));
modeToSetActive = availableModes[dist(randomEngine_)];
LOGI("Random mode active: Chose animation %d", static_cast<int>(modeToSetActive));
} else {
activeAnimationMode_ = configuredAnimationMode_;
modeToSetActive = configuredAnimationMode_;
}
activeAnimationMode_ = modeToSetActive;
// 결정된 모드에 맞는 '전문 요리사(전략 객체)'를 고용
switch (activeAnimationMode_) {
case AnimationMode::PAN:
animationStrategy_ = std::make_unique<PanAnimation>(animationSpeed_);
break;
case AnimationMode::PAN_ONE_WAY:
animationStrategy_ = std::make_unique<PanOneWayAnimation>(animationSpeed_);
break;
case AnimationMode::ZOOM:
animationStrategy_ = std::make_unique<ZoomAnimation>(animationSpeed_);
break;
case AnimationMode::PAGE_TURN:
animationStrategy_ = std::make_unique<PageTurnAnimation>(animationSpeed_, pageTurnDelayMs_);
break;
case AnimationMode::NONE:
default:
animationStrategy_ = std::make_unique<ZoomAnimation>(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<float>(media.getWidth()) / media.getHeight();
float surfaceAspect = static_cast<float>(surfaceWidth) / surfaceHeight;
if (mediaAspect > surfaceAspect) {
outScale = static_cast<float>(surfaceHeight) / media.getHeight();
outOffsetX = (static_cast<float>(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<int>(configuredAnimationMode_) << "\n";
ss << " Active Mode : " << static_cast<int>(activeAnimationMode_) << "\n";
ss << " Fading: " << (isFading_ ? "YES" : "NO") << "\n";
// ss << " Transitioning: " << (isTransitioning_ ? "YES" : "NO") << "\n"; // <-- 삭제
ss << " Configured Anim: " << static_cast<int>(configuredAnimationMode_) << "\n";
ss << " Active Anim : " << static_cast<int>(activeAnimationMode_) << "\n";
ss << " Configured Trans: " << static_cast<int>(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<std::mutex> 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<std::chrono::milliseconds>(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<std::chrono::milliseconds>(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<float>(currentMedia_.getWidth());
float mediaH = static_cast<float>(currentMedia_.getHeight());
// 화면 비율에 맞춰 확대되었을 때, 넘치는 부분 계산
if ((mediaW / mediaH) > (static_cast<float>(surfaceWidth) / surfaceHeight)) {
float scale = static_cast<float>(surfaceHeight) / mediaH;
overflowX = std::max(0.0f, mediaW * scale - surfaceWidth);
} else {
float scale = static_cast<float>(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<std::chrono::milliseconds>(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<float>(currentMedia_.getWidth());
float mediaH = static_cast<float>(currentMedia_.getHeight());
if ((mediaW / mediaH) > (static_cast<float>(surfaceWidth) / surfaceHeight)) {
float scale = static_cast<float>(surfaceHeight) / mediaH;
overflowX = std::max(0.0f, mediaW * scale - surfaceWidth);
} else {
LOGW("감독: 페이드 완료되었으나 다음 배우가 준비되지 않음.");
float scale = static_cast<float>(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<int> dist(0, 1); // 0:FADE, 1:SLIDE
transModeToUse = static_cast<TransitionMode>(dist(randomEngine_));
}
// 모드에 맞는 전환 전문가 객체 생성
if (transModeToUse == TransitionMode::SLIDE) {
transitionStrategy_ = std::make_unique<SlideTransition>(fadeDurationMs_, surfaceWidth);
} else if (transModeToUse == TransitionMode::FADE) { // 기본값 및 MOSAIC 대체는 FADE
transitionStrategy_ = std::make_unique<FadeTransition>(fadeDurationMs_);
}else if (transModeToUse == TransitionMode::MOSAIC) { // 기본값 및 MOSAIC 대체는 FADE
transitionStrategy_ = std::make_unique<MosaicTransition>(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<std::chrono::milliseconds>(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<float>(currentMedia_.getWidth()) / currentMedia_.getHeight()) > (static_cast<float>(surfaceWidth) / surfaceHeight)) {
scale = static_cast<float>(surfaceHeight) / currentMedia_.getHeight();
} else {
scale = static_cast<float>(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<uint8_t>(alpha * 255.0f);
for (int y = 0; y < buffer.height; ++y) {
int srcY = static_cast<int>((y - offsetY) / scale);
// --- ⬇️ 이 부분이 최종 수정된 올바른 좌표 계산입니다 ⬇️ ---
int srcY = static_cast<int>((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<int>((x - offsetX) / scale);
// --- ⬇️ 이 부분이 최종 수정된 올바른 좌표 계산입니다 ⬇️ ---
int srcX = static_cast<int>((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);
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "MediaAsset.h"
#include "AnimationStrategy.h"
#include "TransitionStrategy.h"
#include "Preloader.h"
#include <string>
#include <sstream>
@ -8,11 +9,13 @@
#include <chrono>
#include <mutex>
#include <random>
#include <memory> // for std::unique_ptr
#include <android/native_window.h>
#include <android/native_window_jni.h>
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> animationStrategy_;
std::unique_ptr<TransitionStrategy> 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();
};

View File

@ -0,0 +1,160 @@
#include "TransitionStrategy.h"
#include "Renderer.h"
#include <algorithm> // for std::clamp
#include <vector>
#include <numeric> // for std::iota
#include <random> // 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<int>(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<int>(x / tileWidth);
int tileY = static_cast<int>(y / tileHeight);
int tileIndex = tileY * tilesX_ + tileX;
// 이 타일이 몇 번째로 드러나야 하는지(rank) 확인
int rank = revealOrder_[tileIndex];
const uint8_t* finalPixelData;
if (rank < tilesToReveal) {
// 드러나야 할 타일이면, 새 이미지(dest)에서 픽셀을 가져옴
int srcX = static_cast<int>((x + destOffsetX) / destScale);
int srcY = static_cast<int>((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<int>((x + srcOffsetX) / srcScale);
int srcY = static_cast<int>((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<int> tileOrder_;
std::vector<int> revealOrder_; // 역순 조회 테이블
std::mt19937& randomEngine_;
};

View File

@ -0,0 +1,36 @@
#pragma once
#include <android/native_window.h>
#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_;
};

View File

@ -0,0 +1,33 @@
//
// Created by JIBUM HAN on 2025. 8. 28..
//
#include "AnimationStrategy.h"
#include <algorithm> // 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

View File

@ -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<Renderer>(nativeHandle);
if (renderer) {
renderer->setTransitionMode(mode);
}
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetFadeDuration(JNIEnv* env, jobject, jlong nativeHandle, jint duration) {

View File

@ -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

View File

@ -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)