【HTML】SVG タグで遊んでみた

こんにちは、しましまです。
今回は、HTMLのSVGタグに触れる機会がありましたので、そのことについてとりあげます。

きっかけ

きっかけは個人的なツール作りで、時間管理をするものがほしいと思いました。
ツールでやりたいことの一つとして、「入力した開始時刻と終了時刻を可視化したい」という思いがあり、
それを実現できるライブラリを探しましたが、これといったものが見つかりませんでした。
その解決案として SVG タグで実現しようと考え、その方針にしました。

コード

今回は Vue.js で作成しました。

HTML

  <svg width="300" height="300" viewBox="0 0 300 300">
    <!-- 時計盤 -->
    <circle cx="150" cy="150" r="140" fill="#fff" stroke="#ccc" stroke-width="2" />

    <!-- ハイライトエリア -->
    <path :d="arcPath" fill="rgba(64, 186, 141, 0.8)" />

    <!-- start 針 -->
    <line :x2="startX" :y2="startY" x1="150" y1="150" stroke="#0067C0" stroke-width="4" />

    <!-- end 針 -->
    <line :x2="endX" :y2="endY" x1="150" y1="150" stroke="#d9333f" stroke-width="4" />

    <!-- 中心点 -->
    <circle cx="150" cy="150" r="5" fill="#000" />
  </svg>

Script

const props = defineProps({
  start: { type: String, required: true },
  end: { type: String, required: true },
})

const parseTime = (timeStr) => {
  const [h, m] = timeStr.split(':').map(Number)
  return h + m / 60
}

const toCoord = (hours, radius = 100) => {
  const angleDeg = (hours % 12) * 30 - 90 // 0時を上に
  const angleRad = (angleDeg * Math.PI) / 180
  const x = 150 + radius * Math.cos(angleRad)
  const y = 150 + radius * Math.sin(angleRad)
  return { x, y }
}

const startHours = computed(() => parseTime(props.start))
const endHours = computed(() => parseTime(props.end))

const startCoord = computed(() => toCoord(startHours.value))
const endCoord = computed(() => toCoord(endHours.value))

const startX = computed(() => startCoord.value.x)
const startY = computed(() => startCoord.value.y)
const endX = computed(() => endCoord.value.x)
const endY = computed(() => endCoord.value.y)

// 時計盤上のハイライト扇形(start → end 方向に描く)
const arcPath = computed(() => {
  const r = 120
  const startAngle = (startHours.value % 12) * 30 - 90
  const endAngle = (endHours.value % 12) * 30 - 90

  const start = {
    x: 150 + r * Math.cos((startAngle * Math.PI) / 180),
    y: 150 + r * Math.sin((startAngle * Math.PI) / 180),
  }

  const end = {
    x: 150 + r * Math.cos((endAngle * Math.PI) / 180),
    y: 150 + r * Math.sin((endAngle * Math.PI) / 180),
  }

  const largeArcFlag = (endHours.value - startHours.value + 12) % 12 > 6 ? 1 : 0

  return `
    M 150 150
    L ${start.x} ${start.y}
    A ${r} ${r} 0 ${largeArcFlag} 1 ${end.x} ${end.y}
    Z
  `
})

動作イメージ(9:00 – 14:00)

おわりに

自由度が高く、できることが多い印象を受けました。
まだ慣れていないためか、保守するのは少ししんどそうな気がしています。

困ったときの手段として引き出しにしまっておきます。