HTML・CSS・JSで円のアニメーション付きのカウントダウンタイマーを作ろう

# 完成品
時間の中に入力する値は1~99であることを前提にしている

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="OJGejqV"</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# code

## HTML
```index.html
<body>
  <div id="selects">
    <div>
      <label for="radius">半径：</label>
      <input type="range" id="radius" name="radius" min="50" max="500" value="200" oninput="changeRadius(this.value);">
    </div>
    <!-- 追加：ボタン -->
    <div>
      <button onclick="startAnimation()">Start Animation</button>
      <button onclick="all_reset()">Reset Animation</button>
    </div>
    <div>
      <label for="radius">時間：</label>
      <input type="number" id="time" name="time" min="1" max="99" value="60" oninput="changeTimes(this.value);">
    </div>
  </div>
  <div class="circle2">
    <div class="triangle1" id="triangle1"></div>
    <div class="triangle2" id="triangle2"></div>
    <div class="triangle3" id="triangle3"></div>
    <div class="triangle4" id="triangle4"></div>
    <div class="triangle5" id="triangle5"></div>
    <div class="triangle6" id="triangle6"></div>
  </div>
  <!-- 追加：秒数表示 -->
  <div class="circle">

  </div>
  <div id="count_box">
    <p id="currentSeconds">0</p>
  </div>
</body>
```
## CSS
細かな説明が気になる方は以下を参照ください

https://qiita.com/zono0013/items/f6d9657c043b5a52af3d

```index.css
:root {
    --radius: 200px; /* デフォルトの半径を定義 */
  }
  body {
    display: grid;
    place-items: center;
    height: calc(100vh - 16px);
    width: calc(100vw - 16px);
    grid-template-columns: repeat(5, 1fr);
    grid-template-rows: repeat(5, 1fr);
  }
  #selects {
    height: 100%;
    width: 100%;
    grid-column: 1/6;
    grid-row: 1/2;
    display: grid;
    align-items: center;
    justify-content: center;
    padding: 20px;
  }
  .circle {
    width: 175px;
    height: 175px;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    grid-column: 2/5;
    grid-row: 2/5;
    background-color: black; /* 円の背景色 */
    border-radius: 50%; /* 円形にする */
    position: relative;
  }
  .circle2 {
    width: 200px;
    height: 200px;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    grid-column: 2/5;
    grid-row: 2/5;
    background-color: red; /* 円の背景色 */
    border-radius: 50%; /* 円形にする */
    position: relative;
    overflow: hidden;
  }
  .triangle1{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }
  .triangle2{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }
  .triangle3{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }
  .triangle4{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }
  .triangle5{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }
  .triangle6{
    height: auto;
    width: auto;
    display: grid;
    grid-column: 1/2;
    grid-row: 1/2;
  }

  .triangle1::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))));
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .triangle2::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg)))) rotate(60deg);
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .triangle3::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg)))) rotate(120deg);
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .triangle4::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg)))) rotate(180deg);
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .triangle5::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg)))) rotate(240deg);
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .triangle6::before {
    content: "";
    width: 0;
    height: 0;
    grid-column: 1/2;
    grid-row: 1/2;
    transform-origin: left bottom; /* 中心を縦横の中心に設定 */
    transform: translate(calc(var(--radius) / 2), calc(var(--radius) / 2 - var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg)))) rotate(300deg);
    border-right: calc(var(--radius) * 1.25 * sin(calc(var(--angle) * 1deg))) solid transparent;
    border-top: calc(var(--radius) * 1.25 * cos(calc(var(--angle) * 1deg))) solid #f3c87d;
  }
  .currentSeconds{
    grid-column: 1/2;
    grid-row: 1/2;
  }
  #count_box{
    display: grid;
    place-items: center;
    height: var(--radius);
    width: var(--radius);
    grid-column: 2/5;
    grid-row: 2/5;
    font-size: calc(var(--radius) / 2);
    font-weight: bold;
    z-index: 1;
  }
  p{
    color: white;
    margin: 0;
  }
```
## JS
`smoothTransition()`で**滑らかな**アニメーショにする際，**正確な時間**を表示するために**スタート時刻との差から計算した角度**を`changeAngle(angle, times_animat)`で使用している．

```index.js
var times = 60; // 残り時間
changeTimes(times);
var reset_times = false; // アニメーションをリセットするかどうか

// 時間を変更する関数
function changeTimes(time){
  updateCurrentSeconds(time);
}

// アニメーション開始関数
function startAnimation() {
  if(!reset_times){
    reset_times = true;
    smoothTransition(); // 360度までの滑らかな遷移
  }
}

// 秒数を表示する関数
function updateCurrentSeconds(seconds) {
  document.getElementById('currentSeconds').textContent = Math.floor(seconds);
}

// 中心角を変更する関数
function changeAngle(angle, times_animat){
  if(angle < 65){
    document.getElementById('triangle1').style.setProperty('--angle', angle);
  }else if(angle < 125){
    document.getElementById('triangle2').style.setProperty('--angle', angle - 60);
  }else if(angle < 185){
    document.getElementById('triangle3').style.setProperty('--angle', angle - 120);
  }else if(angle < 245){
    document.getElementById('triangle4').style.setProperty('--angle', angle - 180);
  }else if(angle < 305){
    document.getElementById('triangle5').style.setProperty('--angle', angle - 240);
  }else if(angle < 360){
    if(angle < 360){
      document.getElementById('triangle6').style.setProperty('--angle', angle - 300);
    }else{
      document.getElementById('triangle6').style.setProperty('--angle', 65);
    }
  }else{
    reset_angle();
  }
  // 現在の秒数を更新
  if (times_animat - (angle / 360 * times_animat) > 0) {
      updateCurrentSeconds(times_animat - (angle / 360 * times_animat));
  } else {
      updateCurrentSeconds(0);
  }
}

// 中心角のリセット
function reset_angle(){
  document.getElementById('triangle1').style.setProperty('--angle', 0);
  document.getElementById('triangle2').style.setProperty('--angle', 0);
  document.getElementById('triangle3').style.setProperty('--angle', 0);
  document.getElementById('triangle4').style.setProperty('--angle', 0);
  document.getElementById('triangle5').style.setProperty('--angle', 0);
  document.getElementById('triangle6').style.setProperty('--angle', 0);
  return true;
}

// 初期化
function all_reset(){
  reset_times = false;
}

// 滑らかなアニメーション
function smoothTransition() {
  var currentAngle = 0; // 現在の角度
  var times_animat = document.getElementById('time').value; // カウント時間
  var duration = 1000 * times_animat; // アニメーションの期間（ミリ秒）
  
  var startTime = performance.now(); // アニメーションの開始時間

  var animation = setInterval(function() {
    var elapsed = performance.now() - startTime; // 経過時間

    // 現在の角度を更新
    currentAngle = (360) * (elapsed / duration);
    // 中心角を計算して更新 
    changeAngle(currentAngle, times_animat);

    // アニメーションが終了したら終了
    if (elapsed >= duration || !reset_times) {
      clearInterval(animation);
      updateCurrentSeconds(times_animat);
      reset_angle();
      reset_times = false;
    }
  }, 1000 / 60); // 60 FPSでアニメーション
}
```