feat: [academy] 방화셔터 동작 애니메이션 추가
- 인라인 SVG + CSS @keyframes 애니메이션 - 감김→1차하강→완전폐쇄→복귀 루프 - 재생/정지/리셋 컨트롤, 상태 텍스트 표시
This commit is contained in:
@@ -83,6 +83,20 @@
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* 셔터 동작 애니메이션 */
|
||||
@keyframes shutter-move {
|
||||
0%, 5% { transform: translateY(-150px); }
|
||||
35%, 45% { transform: translateY(0px); }
|
||||
65%, 75% { transform: translateY(45px); }
|
||||
95%, 100%{ transform: translateY(-150px); }
|
||||
}
|
||||
.shutter-curtain {
|
||||
animation: shutter-move 8s ease-in-out infinite;
|
||||
}
|
||||
.shutter-curtain.paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@@ -202,6 +216,95 @@ class="w-full rounded-lg cursor-pointer academy-img-hover"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 셔터 동작 애니메이션 -->
|
||||
<div class="mt-6 mb-2">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
셔터 동작 시뮬레이션
|
||||
</h4>
|
||||
<div class="bg-gray-50 rounded-xl p-4 border" style="max-width: 480px;">
|
||||
<svg viewBox="0 0 300 260" class="w-full" style="max-width: 400px; display: block; margin: 0 auto;">
|
||||
<defs>
|
||||
<clipPath id="shutter-clip">
|
||||
<rect x="83" y="70" width="134" height="150"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- 배경 -->
|
||||
<rect x="0" y="0" width="300" height="260" fill="#fef2f2" rx="8"/>
|
||||
|
||||
<!-- 천장 -->
|
||||
<rect x="60" y="25" width="180" height="10" fill="#d1d5db" stroke="#9ca3af" stroke-width="1" rx="2"/>
|
||||
|
||||
<!-- 케이스 -->
|
||||
<rect x="75" y="35" width="150" height="35" fill="#fee2e2" stroke="#dc2626" stroke-width="1" rx="3"/>
|
||||
<!-- 샤프트 (롤) -->
|
||||
<circle cx="150" cy="52" r="10" fill="#fecdd3" stroke="#dc2626" stroke-width="1"/>
|
||||
<circle cx="150" cy="52" r="3" fill="#dc2626"/>
|
||||
|
||||
<!-- 가이드레일 좌/우 -->
|
||||
<rect x="78" y="70" width="5" height="150" fill="#fca5a5" stroke="#dc2626" stroke-width="0.5" rx="1"/>
|
||||
<rect x="217" y="70" width="5" height="150" fill="#fca5a5" stroke="#dc2626" stroke-width="0.5" rx="1"/>
|
||||
|
||||
<!-- 바닥 -->
|
||||
<rect x="60" y="220" width="180" height="8" fill="#d1d5db" stroke="#9ca3af" stroke-width="1" rx="2"/>
|
||||
|
||||
<!-- 1차 정지선 (점선) -->
|
||||
<line x1="83" y1="175" x2="217" y2="175" stroke="#f59e0b" stroke-width="0.5" stroke-dasharray="4,3" opacity="0.5"/>
|
||||
<text x="228" y="178" font-size="7" fill="#d97706" font-family="sans-serif" opacity="0.7">1차 정지</text>
|
||||
|
||||
<!-- 슬랫 커튼 (애니메이션) -->
|
||||
<g clip-path="url(#shutter-clip)">
|
||||
<g class="shutter-curtain paused">
|
||||
@for ($i = 0; $i < 16; $i++)
|
||||
<rect x="83" y="{{ 59 + $i * 7 }}" width="134" height="6"
|
||||
fill="{{ $i % 2 === 0 ? '#fecdd3' : '#ffe4e6' }}"
|
||||
stroke="#e11d48" stroke-width="0.5" rx="1"/>
|
||||
@endfor
|
||||
<!-- 하단바 -->
|
||||
<rect x="80" y="171" width="140" height="4" fill="#dc2626" stroke="#991b1b" stroke-width="0.5" rx="1"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 라벨 -->
|
||||
<text x="255" y="33" font-size="8" fill="#9ca3af" text-anchor="middle" font-family="sans-serif">천장</text>
|
||||
<text x="255" y="55" font-size="8" fill="#9ca3af" text-anchor="middle" font-family="sans-serif">케이스</text>
|
||||
<text x="42" y="145" font-size="8" fill="#9ca3af" text-anchor="middle" font-family="sans-serif">가이드</text>
|
||||
<text x="42" y="155" font-size="8" fill="#9ca3af" text-anchor="middle" font-family="sans-serif">레일</text>
|
||||
<text x="255" y="227" font-size="8" fill="#9ca3af" text-anchor="middle" font-family="sans-serif">바닥</text>
|
||||
|
||||
<!-- 단계 표시 -->
|
||||
<g font-family="sans-serif">
|
||||
<text x="18" y="95" font-size="7" fill="#dc2626" text-anchor="middle">연기</text>
|
||||
<text x="18" y="104" font-size="7" fill="#dc2626" text-anchor="middle">감지</text>
|
||||
<line x1="30" y1="98" x2="75" y2="98" stroke="#dc2626" stroke-width="0.5" stroke-dasharray="2,2" opacity="0.4"/>
|
||||
|
||||
<text x="18" y="200" font-size="7" fill="#ef4444" text-anchor="middle">열</text>
|
||||
<text x="18" y="209" font-size="7" fill="#ef4444" text-anchor="middle">감지</text>
|
||||
<line x1="30" y1="203" x2="75" y2="203" stroke="#ef4444" stroke-width="0.5" stroke-dasharray="2,2" opacity="0.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- 컨트롤 -->
|
||||
<div class="flex items-center gap-3 mt-3">
|
||||
<button onclick="toggleShutterAnim()" id="shutter-play-btn"
|
||||
class="px-3 py-1.5 bg-red-500 text-white text-xs rounded-lg hover:bg-red-600 transition-colors font-medium">
|
||||
▶ 재생
|
||||
</button>
|
||||
<button onclick="resetShutterAnim()"
|
||||
class="px-3 py-1.5 bg-gray-200 text-gray-600 text-xs rounded-lg hover:bg-gray-300 transition-colors">
|
||||
↻ 처음으로
|
||||
</button>
|
||||
<span id="shutter-status" class="text-xs font-medium text-green-600">
|
||||
상태: 평상시
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1-2. 종류 + 11.png (강판형 vs 스크린형) -->
|
||||
@@ -1234,6 +1337,71 @@ function hidePreview() {
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// 셔터 동작 애니메이션 컨트롤
|
||||
(function() {
|
||||
var shutterStatusTimer = null;
|
||||
|
||||
window.toggleShutterAnim = function() {
|
||||
var curtain = document.querySelector('.shutter-curtain');
|
||||
var btn = document.getElementById('shutter-play-btn');
|
||||
if (!curtain || !btn) return;
|
||||
|
||||
curtain.classList.toggle('paused');
|
||||
var isPaused = curtain.classList.contains('paused');
|
||||
btn.textContent = isPaused ? '▶ 재생' : '⏸ 정지';
|
||||
|
||||
if (!isPaused) {
|
||||
startStatusUpdater();
|
||||
} else {
|
||||
clearInterval(shutterStatusTimer);
|
||||
}
|
||||
};
|
||||
|
||||
window.resetShutterAnim = function() {
|
||||
var curtain = document.querySelector('.shutter-curtain');
|
||||
var btn = document.getElementById('shutter-play-btn');
|
||||
var status = document.getElementById('shutter-status');
|
||||
if (!curtain) return;
|
||||
|
||||
curtain.style.animation = 'none';
|
||||
curtain.offsetHeight; // reflow 강제
|
||||
curtain.style.animation = '';
|
||||
curtain.classList.add('paused');
|
||||
if (btn) btn.textContent = '▶ 재생';
|
||||
if (status) {
|
||||
status.textContent = '상태: 평상시';
|
||||
status.className = 'text-xs font-medium text-green-600';
|
||||
}
|
||||
clearInterval(shutterStatusTimer);
|
||||
};
|
||||
|
||||
function startStatusUpdater() {
|
||||
clearInterval(shutterStatusTimer);
|
||||
shutterStatusTimer = setInterval(function() {
|
||||
var curtain = document.querySelector('.shutter-curtain');
|
||||
var status = document.getElementById('shutter-status');
|
||||
if (!curtain || !status || curtain.classList.contains('paused')) return;
|
||||
|
||||
try {
|
||||
var style = window.getComputedStyle(curtain);
|
||||
var matrix = new DOMMatrix(style.transform);
|
||||
var ty = matrix.m42;
|
||||
|
||||
if (ty < -80) {
|
||||
status.textContent = '상태: 평상시';
|
||||
status.className = 'text-xs font-medium text-green-600';
|
||||
} else if (ty < 20) {
|
||||
status.textContent = '상태: 1차 하강 (일부폐쇄)';
|
||||
status.className = 'text-xs font-medium text-amber-600';
|
||||
} else {
|
||||
status.textContent = '상태: 완전 폐쇄';
|
||||
status.className = 'text-xs font-medium text-red-600';
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 150);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@include('components.academy-glossary', ['domain' => 'fire-shutter'])
|
||||
|
||||
Reference in New Issue
Block a user