mirror of
https://github.com/headporter81/specialsource-homepage-frontend.git
synced 2026-08-08 15:21:11 +09:00
This commit is contained in:
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
// [Props 정의]
|
||||||
|
const props = defineProps({
|
||||||
|
// 현재 진행 값
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
// 총량
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
// 하단에 표시할 상태 메시지
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: 'Initializing...'
|
||||||
|
},
|
||||||
|
// 막대의 총 글자 길이
|
||||||
|
barLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// [핵심 로직: Computed로 실시간 계산]
|
||||||
|
|
||||||
|
// 1. 퍼센트 계산 (0 ~ 100, 소수점 버림)
|
||||||
|
const percentage = computed(() => {
|
||||||
|
if (props.total <= 0) return 0;
|
||||||
|
const percent = (props.progress / props.total) * 100;
|
||||||
|
return Math.min(Math.round(percent), 100); // 100%를 넘지 않도록 제한
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. ASCII 막대 문자열 생성 (█░ 문자 조합)
|
||||||
|
const barString = computed(() => {
|
||||||
|
const filledCount = Math.round((props.barLength * percentage.value) / 100);
|
||||||
|
const emptyCount = props.barLength - filledCount;
|
||||||
|
|
||||||
|
// '.repeat()'를 활용해 칼각 유지
|
||||||
|
const filledBar = '█'.repeat(filledCount);
|
||||||
|
const emptyBar = '░'.repeat(emptyCount);
|
||||||
|
|
||||||
|
return `${filledBar}${emptyBar}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tui-window">
|
||||||
|
<div class="tui-header">
|
||||||
|
<span class="tui-prompt">porter@specialsource-nas:~$</span>
|
||||||
|
<span class="tui-cmd">deploy-service --watch</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tui-bar-row">
|
||||||
|
<span class="tui-bracket">[</span>
|
||||||
|
<span class="tui-bar">{{ barString }}</span>
|
||||||
|
<span class="tui-bracket">]</span>
|
||||||
|
<span class="tui-percent">{{ percentage.toString().padStart(3, ' ') }}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tui-status-row">
|
||||||
|
<span class="tui-label">>> Status:</span>
|
||||||
|
<span class="tui-message">{{ status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* [TUI 핵심 스타일 CSS] */
|
||||||
|
|
||||||
|
.tui-window {
|
||||||
|
/* 레트로 터미널 배경색 및 초록색 글씨 */
|
||||||
|
background-color: #000000;
|
||||||
|
color: #33ff33; /* 형광 초록 */
|
||||||
|
|
||||||
|
/* ★가장 중요: 칼각을 위한 고정폭 폰트 설정 */
|
||||||
|
font-family: 'Courier New', 'Fira Code', 'Monaco', monospace;
|
||||||
|
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #33ff33;
|
||||||
|
box-shadow: 0 0 15px rgba(51, 255, 51, 0.2);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 520px; /* 폰트에 따라 깨지지 않게 최소폭 지정 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-header {
|
||||||
|
color: #aaaaaa; /* 프롬프트는 약간 흐리게 */
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-cmd {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-bar-row {
|
||||||
|
/* 막대가 스르륵 차오르는 느낌을 위한 자간 및 폰트 크기 조정 */
|
||||||
|
font-size: 1.25rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
/* █와 ░의 높이를 맞추기 위해 white-space 설정 */
|
||||||
|
white-space: pre;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-bracket {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-bar {
|
||||||
|
/* 막대 문자 자체에 약간의 글로우 효과 */
|
||||||
|
text-shadow: 0 0 5px rgba(51, 255, 51, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-percent {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-status-row {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-top: 1px dotted #444;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-label {
|
||||||
|
color: #888888;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-message {
|
||||||
|
color: #ffffff;
|
||||||
|
/* 메시지가 길어질 경우 대비 */
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+115
-53
@@ -140,24 +140,35 @@
|
|||||||
<div ref="terminalRef" class="terminal-box"></div>
|
<div ref="terminalRef" class="terminal-box"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="isProcessing" class="tui-global-overlay">
|
||||||
|
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import TuiProgressBar from '@/components/TuiProgressBar.vue';
|
||||||
|
|
||||||
const videos = ref([]);
|
const videos = ref([]);
|
||||||
const terminalRef = ref(null);
|
const terminalRef = ref(null);
|
||||||
const termInstance = ref(null);
|
const termInstance = ref(null);
|
||||||
|
|
||||||
// 로그인 폼 초기값 세팅 유지
|
|
||||||
const loginId = ref('');
|
const loginId = ref('');
|
||||||
const loginPw = ref('');
|
const loginPw = ref('');
|
||||||
|
|
||||||
const activeMenu = ref('main');
|
const activeMenu = ref('main');
|
||||||
const authToken = ref(localStorage.getItem('token'));
|
const authToken = ref(localStorage.getItem('token'));
|
||||||
|
|
||||||
|
// 글로벌 오버레이 레이어 상태 관리 변수
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
const overlayProgress = ref(0);
|
||||||
|
const overlayStatus = ref('');
|
||||||
|
|
||||||
// 메뉴 아이템 배열
|
// 메뉴 아이템 배열
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: 'main', name: 'HOME' },
|
{ id: 'main', name: 'HOME' },
|
||||||
@@ -169,11 +180,72 @@ const menuItems = [
|
|||||||
|
|
||||||
const isLoggedIn = computed(() => !!authToken.value);
|
const isLoggedIn = computed(() => !!authToken.value);
|
||||||
|
|
||||||
|
// 🟢 [UPGRADE] 메뉴 이동 시 영화 같은 TUI 로딩바 시퀀스 구동
|
||||||
const changeMenu = (id) => {
|
const changeMenu = (id) => {
|
||||||
activeMenu.value = id;
|
// 이미 열려있는 메뉴를 다시 누르면 불필요한 시퀀스 방지 차단
|
||||||
|
if (activeMenu.value === id) return;
|
||||||
|
|
||||||
|
// 1) 즉시 화면 전체를 덮고 초기 세팅
|
||||||
|
isProcessing.value = true;
|
||||||
|
overlayProgress.value = 0;
|
||||||
|
overlayStatus.value = `INITIALIZING ROUTE ACCESS: /${id.toUpperCase()}...`;
|
||||||
|
|
||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`\n[[b;green;]>] NAVIGATING TO /${id.toUpperCase()}... SUCCESS.`);
|
termInstance.value.echo(`\n[[b;green;]>] NAVIGATING TO /${id.toUpperCase()}... INITIALIZING PIPELINE.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) 400ms 동안 빠르게 게이지를 채우는 인터벌 구동 (속도 체감 굿)
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
overlayProgress.value += 5;
|
||||||
|
|
||||||
|
// 진행 수치별 리눅스 해커 감성 문구 전환
|
||||||
|
if (overlayProgress.value === 25) {
|
||||||
|
overlayStatus.value = `ALLOCATING MEMORY BUFFER FOR VUE_COMPONENT...`;
|
||||||
|
}
|
||||||
|
if (overlayProgress.value === 55) {
|
||||||
|
overlayStatus.value = `DECRYPTING INTERFACE LOGS & SECURE ASSETS...`;
|
||||||
|
}
|
||||||
|
if (overlayProgress.value === 85) {
|
||||||
|
// ★ 85% 지점 오버레이 베일 뒤에서 슥 메인 콘텐츠 메뉴를 스왑!
|
||||||
|
activeMenu.value = id;
|
||||||
|
overlayStatus.value = `MOUNTING VIRTUAL DOM TREE... SUCCESS.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 100% 도달 완료 시 종료 세팅
|
||||||
|
if (overlayProgress.value >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
if (termInstance.value) {
|
||||||
|
termInstance.value.echo(`[[b;green;][SUCCESS]] COMPONENT LOADED PERFECTLY.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 여운을 약간 준 뒤 부드럽게 창 닫기
|
||||||
|
setTimeout(() => {
|
||||||
|
isProcessing.value = false;
|
||||||
|
}, 120);
|
||||||
|
}
|
||||||
|
}, 20); // 20ms * 20단계 = 총 400ms 로딩 스피드 보장
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로그인 등 다른 비즈니스 로직에서 사용하는 동적 제어용 스크립트는 원본 유지
|
||||||
|
const triggerOverlayProcess = (durationMs, initialMsg) => {
|
||||||
|
isProcessing.value = true;
|
||||||
|
overlayProgress.value = 0;
|
||||||
|
overlayStatus.value = initialMsg;
|
||||||
|
|
||||||
|
const steps = 20;
|
||||||
|
const stepTime = durationMs / steps;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (overlayProgress.value >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setTimeout(() => {
|
||||||
|
isProcessing.value = false;
|
||||||
|
}, 150);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
overlayProgress.value += (100 / steps);
|
||||||
|
}, stepTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiKey = 'AIzaSyD51LHwqoY8spjq6rsY_RlhBXzb96j7D6o';
|
const apiKey = 'AIzaSyD51LHwqoY8spjq6rsY_RlhBXzb96j7D6o';
|
||||||
@@ -206,15 +278,21 @@ const handleLoginSubmit = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isProcessing.value = true;
|
||||||
|
overlayProgress.value = 10;
|
||||||
|
overlayStatus.value = 'Transmitting encryption keys to gateway...';
|
||||||
|
|
||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`\n[SYS] INITIALIZING SECURITY GATEWAY CONNECTION...`);
|
termInstance.value.echo(`\n[SYS] INITIALIZING SECURITY GATEWAY CONNECTION...`);
|
||||||
termInstance.value.echo(`[SYS] TRANSMITTING CREDENTIALS [USER: ${loginId.value}]`);
|
termInstance.value.echo(`[SYS] TRANSMITTING CREDENTIALS [USER: ${loginId.value}]`);
|
||||||
termInstance.value.echo(`[SYS] VALIDATING AGAINST CENTRAL POSTGRES_DB CORE...`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
|
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
|
||||||
|
|
||||||
|
overlayProgress.value = 40;
|
||||||
|
overlayStatus.value = 'Validating query signature against PostgreSQL DB...';
|
||||||
|
|
||||||
const response = await axios.post(`${apiBaseUrl}/api/login`, {
|
const response = await axios.post(`${apiBaseUrl}/api/login`, {
|
||||||
username: loginId.value,
|
username: loginId.value,
|
||||||
password: loginPw.value
|
password: loginPw.value
|
||||||
@@ -223,6 +301,9 @@ const handleLoginSubmit = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data && response.data.token) {
|
if (response.data && response.data.token) {
|
||||||
|
overlayProgress.value = 85;
|
||||||
|
overlayStatus.value = 'Access Granted. Generating JWT Session token...';
|
||||||
|
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem('token', response.data.token);
|
||||||
authToken.value = response.data.token;
|
authToken.value = response.data.token;
|
||||||
|
|
||||||
@@ -230,15 +311,22 @@ const handleLoginSubmit = async () => {
|
|||||||
termInstance.value.echo(`[[b;green;][SUCCESS]] ACCESS GRANTED. CREDENTIALS VERIFIED.`);
|
termInstance.value.echo(`[[b;green;][SUCCESS]] ACCESS GRANTED. CREDENTIALS VERIFIED.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overlayProgress.value = 100;
|
||||||
|
overlayStatus.value = 'Session injection complete.';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isProcessing.value = false;
|
||||||
loginId.value = '';
|
loginId.value = '';
|
||||||
loginPw.value = '';
|
loginPw.value = '';
|
||||||
activeMenu.value = 'main';
|
activeMenu.value = 'main';
|
||||||
|
}, 400);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("로그인 실패:", error);
|
console.error("로그인 실패:", error);
|
||||||
|
isProcessing.value = false;
|
||||||
|
|
||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`[[b;red;][CRITICAL ERROR]] ACCESS DENIED! INVALID ACCESS KEY.`);
|
termInstance.value.echo(`[[b;red;][CRITICAL ERROR]] ACCESS DENIED! INVALID ACCESS KEY.`);
|
||||||
termInstance.value.echo(`[[b;red;][WARN]] IP LOGGED. UNAUTHORIZED ATTEMPT REGISTERED.`);
|
|
||||||
}
|
}
|
||||||
alert("인증에 실패했습니다. 액세스 키를 확인하세요.");
|
alert("인증에 실패했습니다. 액세스 키를 확인하세요.");
|
||||||
}
|
}
|
||||||
@@ -332,6 +420,22 @@ onMounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 글로벌 오버레이 배경 암전 처리 레이어 CSS 스타일 */
|
||||||
|
.tui-global-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.88);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
.ascii-container {
|
.ascii-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -346,8 +450,6 @@ onMounted(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|
||||||
/* [베이스 설정] 애니메이션 시작 전 기본 상태 */
|
|
||||||
color: #00ff00 !important;
|
color: #00ff00 !important;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -355,8 +457,6 @@ onMounted(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
/* 🌟 [핵심 변경] 딱 한 번만 실행(1)하고, 마지막 상태 고정(forwards) */
|
|
||||||
animation: once-cyber-glitch-wobble 2.5s linear 1 forwards !important;
|
animation: once-cyber-glitch-wobble 2.5s linear 1 forwards !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,58 +551,20 @@ iframe { width: 100%; height: 180px; border-radius: 4px; }
|
|||||||
}
|
}
|
||||||
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
||||||
|
|
||||||
/* 🟢 [NEW] 딱 한 번 실행 후 최종 상태로 고정되는 "일회성 사이버 글리치" 애니메이션 정의 */
|
/* 일회성 사이버 글리치 키프레임 */
|
||||||
@keyframes once-cyber-glitch-wobble {
|
@keyframes once-cyber-glitch-wobble {
|
||||||
/* [시작 - 25%] 차분한 오리지널 녹색 상태 유지 */
|
0%, 25% { transform: translate(0) scale(1) skewX(0deg); color: #00ff00 !important; text-shadow: 0 0 8px rgba(0, 255, 0, 0.7) !important; opacity: 1; }
|
||||||
0%, 25% {
|
26% { transform: translate(-3px, 1.5px) scale(1.02); color: #ffffff !important; text-shadow: 2px 0 #ff0000, -2px 0 #0000ff !important; }
|
||||||
transform: translate(0) scale(1) skewX(0deg);
|
|
||||||
color: #00ff00 !important;
|
|
||||||
text-shadow: 0 0 8px rgba(0, 255, 0, 0.7) !important;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* [글리치 시퀀스 시작 - 26%] RGB 스플릿 플래시 */
|
|
||||||
26% {
|
|
||||||
transform: translate(-3px, 1.5px) scale(1.02);
|
|
||||||
color: #ffffff !important; /* 하얀색 팝 */
|
|
||||||
text-shadow: 2px 0 #ff0000, -2px 0 #0000ff !important; /* 리얼 RGB 스플릿 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* [초고속 지지직 구간 - 27~32%] 도트 단위 미세 진동 */
|
|
||||||
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: #00ff00 !important; }
|
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: #00ff00 !important; }
|
||||||
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff !important; text-shadow: -3px 0 magenta, 3px 0 cyan;}
|
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff !important; text-shadow: -3px 0 magenta, 3px 0 cyan;}
|
||||||
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff !important; text-shadow: 3px 0 magenta, -3px 0 cyan;}
|
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff !important; text-shadow: 3px 0 magenta, -3px 0 cyan;}
|
||||||
30% { transform: translate(0); color: #00ff00 !important; text-shadow: 0 0 15px rgba(0, 255, 0, 1) !important;}
|
30% { transform: translate(0); color: #00ff00 !important; text-shadow: 0 0 15px rgba(0, 255, 0, 1) !important;}
|
||||||
|
|
||||||
/* [안정화 - 31~50%] 잠깐 정상 상태 */
|
|
||||||
31%, 50% { transform: translate(0); color: #00ff00 !important; text-shadow: 0 0 8px rgba(0, 255, 0, 0.7) !important;}
|
31%, 50% { transform: translate(0); color: #00ff00 !important; text-shadow: 0 0 8px rgba(0, 255, 0, 0.7) !important;}
|
||||||
|
51% { transform: scale(1.05) skewY(1deg); color: #ffffff !important; opacity: 0.05; }
|
||||||
/* [빅 글리치 타이밍 - 51%] 시스템 암전 오류 */
|
53% { opacity: 1; transform: translate(0); color: #00ffff !important; text-shadow: 0 0 20px #00ffff, 0 0 40px rgba(0, 255, 255, 0.8) !important; }
|
||||||
51% {
|
|
||||||
transform: scale(1.05) skewY(1deg);
|
|
||||||
color: #ffffff !important;
|
|
||||||
opacity: 0.05; /* 완전 꺼지기 직전 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* [복구 및 최종 색상 변환 시작 - 53%] 사이버 청록색으로 부활 */
|
|
||||||
53% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(0);
|
|
||||||
color: #00ffff !important; /* 사이버 청록색 */
|
|
||||||
text-shadow: 0 0 20px #00ffff, 0 0 40px rgba(0, 255, 255, 0.8) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* [마지막 지지직 - 90~95%] 최종 정착 전 불안정한 떨림 */
|
|
||||||
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
|
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
|
||||||
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
|
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
|
||||||
95% { transform: translate(0); color: #00ffff !important; text-shadow: 0 0 20px #00ffff !important;}
|
95% { transform: translate(0); color: #00ffff !important; text-shadow: 0 0 20px #00ffff !important;}
|
||||||
|
100% { transform: translate(0) scale(1) skewX(0deg); color: #00ffff !important; text-shadow: 0 0 15px #00ffff, 0 0 30px rgba(0, 255, 255, 0.7) !important; opacity: 1; }
|
||||||
/* 🌟 [최종 상태 고정 - 100%] 영롱한 사이버 청록색(Cyan) 및 강력한 Glow 상태로 영원히 고정 */
|
|
||||||
100% {
|
|
||||||
transform: translate(0) scale(1) skewX(0deg);
|
|
||||||
color: #00ffff !important; /* 👈 최종 고정 색상 */
|
|
||||||
text-shadow: 0 0 15px #00ffff, 0 0 30px rgba(0, 255, 255, 0.7) !important; /* 👈 최종 고정 효과 */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user