평소 비소리를 듣는 것을 좋아해서 시작하게 된 프로젝트.
HTML의 audio 태그를 vanila JS 코드로 제어하고,
setInterval로 input의 value를 제어하는 등의 기능을 구현함.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rainy-Player App</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="src/style/style.css" />
<link rel="shortcut icon" href="/src/favicon.ico" />
</head>
<body>
<div class="app">
<audio id="audioContainer" src="./src//sound/rainSound.MP3">
Your browser does not support audio.
</audio>
<header class="header">
<a href="?"
><h1 class="header-title">
<svg
class="header-logo"
width="50"
height="50"
viewBox="0 0 50 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.1924 15.9033C11.5188 15.8527 11.8209 15.7004 12.0557 15.468C12.2904 15.2357 12.4459 14.9352 12.5 14.6094C13.6807 7.43848 18.9092 3.125 25 3.125C30.6572 3.125 34.4355 6.81152 35.957 10.7168C36.0548 10.9671 36.2161 11.1876 36.425 11.3566C36.6339 11.5257 36.8832 11.6373 37.1484 11.6807C42.0312 12.4766 46.0938 15.7373 46.0938 21.4062C46.0938 27.207 41.3477 31.25 35.5469 31.25H12.6953C7.86133 31.25 3.90625 28.8379 3.90625 23.5156C3.90625 18.7822 7.68262 16.4629 11.1924 15.9033V15.9033Z"
stroke="#3C3C3C"
stroke-width="2.5"
stroke-linejoin="round"
/>
<path
d="M37.5 37.5L31.25 46.875M14.0625 37.5L10.9375 42.1875L14.0625 37.5ZM21.875 37.5L15.625 46.875L21.875 37.5ZM29.6875 37.5L26.5625 42.1875L29.6875 37.5Z"
stroke="#3C3C3C"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
rainy-player
</h1></a
>
</header>
<div class="container">
<div class="date">
<h2>
<span class="t-month">March</span><br />
<span class="t-date">08</span>,<br />
<span class="t-day">monday</span>
</h2>
</div>
<div class="timer">
<div class="timer-setting">
<input
type="number"
name="minutes"
id="minutes"
placeholder="00"
min="0"
max="60"
step="1"
/>
<span>:</span>
<input
type="number"
name="seconds"
id="seconds"
placeholder="00"
min="0"
max="30"
step="1"
/>
</div>
<div class="timer-start">
<button type="submit" class="timer-start-button">
<img class="start-logo" src="./src/svg/play.svg" />
</button>
</div>
<div class="timer-stop hidden">
<button type="submit" class="timer-stop-button">
<img class="stop-logo" src="./src/svg/stop.svg" />
</button>
</div>
</div>
<!-- 재생 시작시 timer가 play-timer로 변환 -->
<div class="play-timer hidden">
<svg
class="track-outline"
width="500"
height="500"
viewBox="0 0 500 500"
fill="#ffffff6e"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="250" cy="250" r="240" />
</svg>
<svg
class="move-outline"
width="500"
height="500"
viewBox="0 0 500 500"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="250" cy="250" r="240" stroke="#000" stroke-width="20" />
</svg>
</div>
<div class="port-land portrait">
<span>portrait</span>
</div>
<!-- 버튼 클릭시 landscape로 변환 -->
</div>
<div class="desc">
<h3 class="desc-head">relax with Rainy Sound 🌧</h3>
<input
type="range"
name="volume"
id="volume"
max="1"
step="0.1"
class="volume"
/>
</div>
<footer class="footer">
<h6>©thisisyjin</h6>
<ul>
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/thisisyjin"
><li>github</li></a
>
<a
target="_blank"
rel="noopener noreferrer"
href="https://mywebproject.tistory.com"
><li>dev blog</li></a
>
<a href="mailto: thisisyjin@naver.com"><li>contact</li></a>
</ul>
</footer>
</div>
<script src="app.js"></script>
</body>
</html>
favicon.ico 제작 후 적용함.
<link rel="shortcut icon" href="/src/favicon.ico" />
* {
margin: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans', sans-serif;
width: 100%;
}
.hidden {
display: none !important;
}
/* Reset CSS */
a {
text-decoration: none;
color: inherit;
}
input {
border: none;
font-family: 'Noto Sans', sans-serif;
font-size: 16px;
width: 1.5em;
}
button {
border: none;
background-color: transparent;
}
ul,
li {
list-style: none;
}
input:focus,
input:active {
outline: none;
box-shadow: none;
}
/* style */
.app {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
margin: 0 10%;
}
#volume {
display: block;
width: 100px;
margin: 0 auto;
}
.header {
width: 100%;
padding: 15px 0;
}
.header-title {
text-align: center;
font-size: 22px;
line-height: 1.3636363636;
font-weight: 400;
letter-spacing: 0.1em;
}
.header-title .header-logo {
position: relative;
top: 18px;
width: 20px;
margin-right: 10px;
}
.container {
position: relative;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
background-image: url(../images/landscape.jpg);
background-repeat: no-repeat;
background-size: cover;
}
.app.port {
background-image: url(../images/portrait.jpg);
background-repeat: no-repeat;
background-size: cover;
z-index: 30;
height: 800px;
}
.app.port .container {
background: none;
}
.app.port .header-title,
.app.port .desc h3 {
color: white;
}
.app.port .header-title .header-logo path {
stroke: white;
}
.app.port .timer {
position: absolute;
top: 35%;
}
/* date */
.date {
position: absolute;
top: 40px;
right: 60px;
font-size: 50px;
line-height: 1.3666666667;
font-weight: 700;
letter-spacing: 0.15em;
color: #fff;
text-align: right;
}
/* timer */
.timer {
position: absolute;
top: 15%;
left: 5%;
width: 400px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
position: relative;
background-color: #ffffff98;
z-index: 11;
}
.timer::after {
visibility: hidden;
content: '';
display: block;
position: absolute;
width: 420px;
height: 420px;
z-index: 10;
background-color: rgba(255, 0, 0, 0.219);
border-radius: 50%;
}
.timer .timer-setting {
/* position: absolute;
top: 200px;
left: 200px;
transform: translate(-50%, -50%); */
width: 400px;
font-size: 40px;
text-align: center;
}
.timer .timer-setting input {
background-color: transparent;
font-size: 60px;
font-weight: 500;
line-height: 1.35;
letter-spacing: -0.03em;
color: #000000b6;
}
.timer .timer-setting input::placeholder {
color: #3c3c3c94;
}
.timer .timer-start {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
text-align: center;
background-color: #fff;
transition: 0.1s all ease-in;
}
.timer .timer-start:hover {
transform: scale(1.1);
}
.timer .timer-stop {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
text-align: center;
background-color: #fff;
transition: 0.1s all ease-in;
}
.timer .timer-stop:hover {
transform: scale(1.1);
}
.port-land {
position: absolute;
bottom: 25px;
right: 60px;
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
background-color: #3c3c3c;
color: #fff;
border-radius: 50%;
font-weight: 700;
transition: 0.2s all ease-in;
}
.portrait:hover {
transform: scale(1.1);
color: #ffffffc9;
background-image: url('../images/portrait.jpg');
background-size: cover;
border: 3px solid #ffffff98;
}
.landscape:hover {
transform: scale(1.1);
background-image: url('../images/landscape.jpg');
background-size: cover;
border: 3px solid #ffffff98;
}
/* desc */
.desc {
padding: 15px 0;
}
.desc h3 {
font-size: 16px;
font-weight: 400;
cursor: pointer;
margin-bottom: 6px;
}
/* footer */
.footer {
width: 100vw;
display: flex;
align-items: center;
justify-content: space-around;
background-color: #3c3c3c;
color: #fff;
}
.footer h6 {
font-size: 16px;
font-weight: 400;
letter-spacing: -0.01em;
color: #ecebb3;
}
.footer ul li {
padding: 10px 20px;
display: inline-block;
margin-right: 30px;
transition: 0.3s color ease-in-out;
}
.footer ul li:hover {
color: #8396a1;
}
li :last-child {
margin-right: 0;
}
/* ======
반응형 웹
======== */
/* 태블릿 */
@media screen and (max-width: 1200px) {
.app {
margin: 0 120px;
}
.date {
right: 35px;
font-size: 38px;
font-weight: 700;
letter-spacing: 0.13em;
}
.portrait {
right: 35px;
width: 80px;
height: 80px;
}
.timer {
top: 20%;
left: 4%;
width: 350px;
height: 350px;
}
.timer .timer-setting {
width: 350px;
}
.timer .timer-setting input {
font-size: 50px;
}
.timer .timer-start {
width: 80px;
height: 80px;
}
}
/* 모바일 */
@media screen and (max-width: 480px) {
.app {
margin: 0;
}
.header,
.desc {
padding: 15px 0;
}
.date {
top: 20px;
right: 15px;
font-size: 28px;
font-weight: 700;
letter-spacing: 0.1em;
}
.portrait {
right: 15px;
width: 58px;
height: 58px;
font-size: 13px;
}
.timer {
top: 40%;
left: 3%;
width: 250px;
height: 250px;
}
.timer .timer-setting {
width: 250px;
}
.timer .timer-setting input {
font-size: 42px;
width: 1.66em;
}
.timer .timer-start {
bottom: 10px;
left: 20px;
width: 58px;
height: 58px;
}
.footer {
padding: 10px 0;
}
.footer ul {
display: none;
}
.footer h6 {
font-size: 12px;
}
}
// 날짜 렌더링
const $month = document.querySelector('.t-month');
const $date = document.querySelector('.t-date');
const $day = document.querySelector('.t-day');
const monthArr = [
'JAN',
'FAB',
'MAR',
'APR',
'MAY',
'JUN',
'JUL',
'AUG',
'SEP',
'OCT',
'NOV',
'DEC',
];
const dayArr = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
];
const today = new Date();
console.log(monthArr[today.getMonth()]);
console.log(dayArr[today.getDay()]);
$month.innerText = monthArr[today.getMonth()];
$date.innerText = today.getDate().toString().padStart(2, '0');
$day.innerText = dayArr[today.getDay()];
// 타이머
const $minutes = document.querySelector('#minutes');
const $seconds = document.querySelector('#seconds');
const $startBtn = document.querySelector('.timer-start');
const $stopBtn = document.querySelector('.timer-stop');
let min = 0;
let sec = 0;
let playing = false;
$minutes.addEventListener('change', (e) => {
min = e.target.value;
});
$seconds.addEventListener('change', (e) => {
sec = e.target.value;
});
$startBtn.addEventListener('click', () => {
if (min === 0 && sec === 0) return;
$startBtn.classList.add('hidden');
$stopBtn.classList.remove('hidden');
$minutes.disabled = true;
$seconds.disabled = true;
playing = true;
countDownStart(min, sec);
startMusic();
min = 0;
sec = 0;
});
// 음악 재생
const countDownStart = (min, sec) => {
const countDown = setInterval(() => {
if ($minutes.value == 0 && $seconds.value == 1) {
endCount();
stopMusic();
}
if (sec === 0) {
sec += 59;
min -= 1;
} else {
sec -= 1;
}
$minutes.value = min;
$seconds.value = sec;
}, 1000);
const endCount = () => {
clearInterval(countDown);
console.log('끝!');
playing = false;
$minutes.disabled = false;
$seconds.disabled = false;
$startBtn.classList.remove('hidden');
$stopBtn.classList.add('hidden');
};
// 중지
$stopBtn.addEventListener('click', () => {
endCount();
stopMusic();
$minutes.value = 0;
$seconds.value = 0;
});
};
// audio 태그 제어
const $audio = document.querySelector('#audioContainer');
const startMusic = () => {
$audio.look = true;
$audio.play();
};
const stopMusic = () => {
$audio.pause();
};
// 볼륨 조절
const $desc = document.querySelector('.desc-head');
const $volumeRange = document.querySelector('.volume');
let volume = $volumeRange.value;
$volumeRange.addEventListener('change', (e) => {
$audio.volume = e.target.value;
});
// 1. 테두리 효과
// Portrait / landscape 변환
const $portland = document.querySelector('.port-land');
const $app = document.querySelector('.app');
let isPortrait = false;
$portland.addEventListener('click', () => {
if (!isPortrait) {
isPortrait = true;
$portland.classList.remove('portrait');
$portland.classList.add('landscape');
} else {
isPortrait = false;
$portland.classList.add('portrait');
$portland.classList.remove('landscape');
}
isPortrait ? $app.classList.add('port') : $app.classList.remove('port');
});
원래는 DOM 정의를 최상단에 한꺼번에 적는것이 좋지만,
기능 구분을 위해서 나는 따로 적어줬음.
disabled = true
를 적용함.볼륨 조절
볼륨 조절은 input:range로 조절함.
초기 설정 값이 있다면 그 값을 볼륨으로 하고,
만약 중간에 input이 change되면 갱신할 수 있도록 eventListener을 추가해줌.
portrait / landscape 변환
사진 변경 + 가로/세로버전 변경.
isPortrait 값을 기준으로 class를 추가 및 삭제함.
모바일 웹 최적화 진행
기본적인 반응형 웹은 구현했으나, 아직 모바일 최적화는 하지 못했음.
좀더 미디어쿼리부분을 수정하여 최적화 진행 예정임.
테두리 효과 or 캔버스 API
처음에는 원이 점점 차는 효과를 넣으려 했으나, 일반 CSS로는 불가능함.
추후 Canvas API를 이용하여 추가할 예정.
비 내리는 효과?
css animation과 svg를 배워서 비내리는 효과도 주면 좋을 것 같음.