Rainy-Player Project

thisisyjin·2022년 6월 3일
0

Dev Log 🐥

목록 보기
20/23
post-thumbnail

Rainy-Player Project

🌐 배포


🎨 기획 및 디자인

  • figma 디자인 툴로 직접 디자인 및 기획함.

🖼 Preview


프로젝트 구상

평소 비소리를 듣는 것을 좋아해서 시작하게 된 프로젝트.

HTML의 audio 태그를 vanila JS 코드로 제어하고,
setInterval로 input의 value를 제어하는 등의 기능을 구현함.

HTML markup

<!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>&copy;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" />

Style 적용

* {
  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;
  }
}

javaScript 코드 작성

// 날짜 렌더링

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 정의를 최상단에 한꺼번에 적는것이 좋지만,
기능 구분을 위해서 나는 따로 적어줬음.

  1. 날짜 렌더링
  • monthArr과 dayArr을 선언해서 getMonth, getDay가 숫자인 점을 이용하여 인덱스로 사용했음.
  • DOM 객체의 innerText에 해당 문자열을 넣어줌으로서 렌더링 완료.
  1. 타이머
  • 사용자가 설정한 분/초를 min/sec 변수에 저장함. -> input이 change 할때마다.
  • startBtn과 stopBtn이 hidden 클래스를 번갈아가며 부여받음
    -> 클릭한 버튼에 따라 재생 및 정지 구분함.
  • 재생한 상태에서는 타이머의 Input을 조작할 수 없도록 disabled = true를 적용함.
  • countDownStart 함수에 input의 value인 min, sec를 전달함.
  1. 음악 재생
  • 카운트다운의 경우에는 1초마다 실행되는 setInterval을 통해 구현함.
  • 0:01 인 경우에는 카운트를 멈추고, 음악을 종료함. (시간이 0:00이 되므로)
  • sec가 0이 되는 순간 sec는 59를 더하고, min은 -1을 해줌.
  1. 음악 중지
  • stopBtn을 누른 경우
  • 타이머가 0:01이 된 순간
    -> 이때, clearInterval을 해주고 / 인풋의 disabled를 풀어줌.
  1. audio 제어
    startMusic => play()
    stopMusic => pause()
  1. 볼륨 조절
    볼륨 조절은 input:range로 조절함.
    초기 설정 값이 있다면 그 값을 볼륨으로 하고,
    만약 중간에 input이 change되면 갱신할 수 있도록 eventListener을 추가해줌.

  2. portrait / landscape 변환
    사진 변경 + 가로/세로버전 변경.
    isPortrait 값을 기준으로 class를 추가 및 삭제함.


추후 추가할 기능

  1. 모바일 웹 최적화 진행
    기본적인 반응형 웹은 구현했으나, 아직 모바일 최적화는 하지 못했음.
    좀더 미디어쿼리부분을 수정하여 최적화 진행 예정임.

  2. 테두리 효과 or 캔버스 API
    처음에는 원이 점점 차는 효과를 넣으려 했으나, 일반 CSS로는 불가능함.
    추후 Canvas API를 이용하여 추가할 예정.

  3. 비 내리는 효과?
    css animation과 svg를 배워서 비내리는 효과도 주면 좋을 것 같음.

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글