0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スピードメータのようなゲージを作る

Posted at

ezgif-5b77e6c6547533.gif

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Radial Gauge Example</title>
    <style>
      .radial-gauge-svg {
        width: 300;
        height: 300;
      }

      .radial-gauge-svg > g > text,
      g > text {
        text-anchor: middle;
        dominant-baseline: alphabetic;
        font-weight: normal;
        stroke: none;
      }

      .radial-gauge-path {
        fill: none;
        stroke: gainsboro;
        stroke-width: 8;
      }

      .radial-gauge-path-filled {
        fill: none;
        stroke: lightskyblue;
        stroke-width: 8;
      }

      .radial-gauge-title-text {
        font-size: 0.28rem;
        text-anchor: middle;
        alignment-baseline: middle;
        dominant-baseline: central;
        fill: gray;
      }

      .radial-gauge-units-text {
        font-size: 0.22rem;
        text-anchor: middle;
        alignment-baseline: middle;
        dominant-baseline: central;
        fill: gray;
      }

      .radial-gauge-value-text {
        font-size: 0.55rem;
        font-family: sans-serif;
        font-weight: normal;
        text-anchor: middle;
        alignment-baseline: middle;
        dominant-baseline: central;
        fill: dimgray;
      }

      .radial-gauge-circle {
        fill: orangered;
      }

      .radial-gauge-needle {
        fill: unset;
        stroke-width: 0.4;
        stroke: orangered;
      }

      .radial-gauge-scale-text {
        font-size: 0.22rem;
        fill: dimgray;
      }

      .radial-gauge-tick {
        stroke: darkgray;
        stroke-width: 0.4;
      }

      .gauge210 {
        display: inline-block;
        border: 0.5px solid darkgray;
        box-sizing: border-box;
      }
      .gauge270 {
        display: inline-block;
        border: 0.5px solid darkgray;
        box-sizing: border-box;
      }
    </style>
    <script>
      ;(function (global, factory) {
        const Gauge = factory(global)

        if (typeof define === 'function' && define.amd) {
          // AMD support
          define(() => Gauge)
        } else if (typeof module === 'object' && module.exports) {
          // CommonJS support
          module.exports = Gauge
        } else {
          // Browser global
          global.Gauge = Gauge
        }
      })(typeof window === 'undefined' ? this : window, (global) => {
        const document = global.document
        const requestAnimationFrame = global.requestAnimationFrame || global.mozRequestAnimationFrame || global.webkitRequestAnimationFrame || global.msRequestAnimationFrame || ((cb) => setTimeout(cb, 1000 / 60))

        const SVG_NS = 'http://www.w3.org/2000/svg'

        /**
         * Animation utility for smooth transitions.
         */
        class Animation {
          constructor(options) {
            this.duration = options.duration
            this.start = options.start || 0
            this.end = options.end
            this.change = this.end - this.start
            this.step = options.step
            this.easing =
              options.easing ||
              ((pos) => {
                if ((pos /= 0.5) < 1) return 0.5 * Math.pow(pos, 3)
                return 0.5 * (Math.pow(pos - 2, 3) + 2)
              })
            this.currentIteration = 1
            this.iterations = 60 * this.duration
          }

          animate() {
            const progress = this.currentIteration / this.iterations
            const value = this.change * this.easing(progress) + this.start
            this.step(value, this.currentIteration)
            this.currentIteration += 1

            if (progress < 1) {
              requestAnimationFrame(() => this.animate())
            }
          }

          startAnimation() {
            requestAnimationFrame(() => this.animate())
          }
        }

        /**
         * Utility functions for SVG and math operations.
         */
        const Utils = {
          createSVGElement(label, attrs, children) {
            const elem = document.createElementNS(SVG_NS, label)
            for (const attrName in attrs) {
              elem.setAttribute(attrName, attrs[attrName])
            }
            if (children) {
              children.forEach((child) => elem.appendChild(child))
            }
            return elem
          },

          getAngle(percentage, gaugeSpanAngle) {
            return (percentage * gaugeSpanAngle) / 100
          },

          normalize(value, min, max) {
            let val = Number(value)
            if (val < 0) val += min
            return Math.min(val, max)
          },

          getValueInPercentage(value, min, max) {
            const newMax = max - min
            const newVal = value - min
            return (100 * newVal) / newMax
          },

          getCartesian(cx, cy, radius, angle) {
            const rad = (angle * Math.PI) / 180
            return {
              x: Math.round((cx + radius * Math.cos(rad)) * 1000) / 1000,
              y: Math.round((cy + radius * Math.sin(rad)) * 1000) / 1000,
            }
          },

          getDialCoords(radius, startAngle, endAngle) {
            const cx = 50
            const cy = 50
            return {
              start: this.getCartesian(cx, cy, radius, startAngle),
              end: this.getCartesian(cx, cy, radius, endAngle),
            }
          },

          pathString(radius, startAngle, endAngle, largeArc = 1) {
            const { start, end } = this.getDialCoords(radius, startAngle, endAngle)
            return ['M', start.x, start.y, 'A', radius, radius, 0, largeArc, 1, end.x, end.y].join(' ')
          },
        }

        /**
         * Gauge class for creating and managing the gauge.
         */
        class Gauge {
          constructor(container, options = {}) {
            this.container = container
            this.options = { ...Gauge.defaultOptions, ...options }
            this.initialize()
          }

          static defaultOptions = {
            gaugeSize: 45,
            offset: 10,

            displayNeedle: true,
            displayScale: true,
            displaySmallScale: true,
            displayValue: true,
            precision: 2,
            title: 'Speed',
            units: 'Km/h',
            gaugeColor: null,
          }

          initialize() {
            const { gaugeSize, offset, value, displayNeedle } = this.options
            this.radius = gaugeSize - offset
            this.createSVGElements()
            this.updateGauge(value)
            if (displayNeedle) this.drawNeedle()
          }

          createSVGElements() {
            const { startAngle, endAngle, title, units, displayNeedle, displayScale, needleY, titleY, unitsY, valueY } = this.options

            // Create SVG elements
            this.titleText = Utils.createSVGElement('text', { x: 50, y: titleY, class: 'radial-gauge-title-text' }, [document.createTextNode(title)])
            this.unitsText = Utils.createSVGElement('text', { x: 50, y: unitsY, class: 'radial-gauge-units-text' }, [document.createTextNode(units)])
            this.valueText = Utils.createSVGElement('text', { x: 50, y: valueY, class: 'radial-gauge-value-text' })

            this.barPath = Utils.createSVGElement('path', { class: 'radial-gauge-path', d: Utils.pathString(this.radius, startAngle, endAngle) })
            this.barFilledPath = Utils.createSVGElement('path', { class: 'radial-gauge-path-filled', d: Utils.pathString(this.radius, startAngle, startAngle) })

            this.rootSvg = Utils.createSVGElement('svg', { viewBox: '0 0 100 100', class: 'radial-gauge-svg' }, [this.barPath, this.barFilledPath, this.valueText, this.titleText, this.unitsText])

            if (displayNeedle) {
              this.rootSvg.appendChild(Utils.createSVGElement('circle', { class: 'radial-gauge-circle', cx: 50, cy: needleY, r: 2 }))
            }

            if (displayScale) {
              this.createScale()
            }

            this.container.appendChild(this.rootSvg)
          }

          createScale() {
            const { gaugeSize, startAngle, endAngle, min, max, ticks, displaySmallScale } = this.options
            const scaleGroup = Utils.createSVGElement('g', { class: 'radial-gauge-scale' })

            const startTick = startAngle + 90
            const factor = (360 - (startAngle - endAngle)) / (ticks * 10)

            for (let n = 0; n <= ticks * 10; n++) {
              let yT = 50 - gaugeSize + gaugeSize / 10 - (gaugeSize > 40 ? 2 : 0)
              let dT = n % 10 === 0 ? 5 : 2
              if (n % 10 === 0) {
                yT -= 3
              }
              if (n % 10 === 0 || displaySmallScale) {
                const tickLine = Utils.createSVGElement('line', {
                  x1: 50,
                  y1: yT,
                  x2: 50,
                  y2: yT + dT,
                  class: 'radial-gauge-tick',
                  transform: `rotate(${n * factor + startTick} 50 50)`,
                })

                scaleGroup.appendChild(tickLine)

                if (n % 10 === 0) {
                  const scaleText = Utils.createSVGElement(
                    'text',
                    {
                      x: 50,
                      y: yT - 1,
                      class: 'radial-gauge-scale-text',
                      transform: `rotate(${n * factor + startTick} 50 50)`,
                    },
                    [document.createTextNode((n * (max / ticks / 10) + min).toFixed(0))],
                  )
                  scaleGroup.appendChild(scaleText)
                }
              }
            }

            this.rootSvg.appendChild(scaleGroup)
          }

          drawNeedle() {
            const { id, needleY } = this.options
            const needleCoord = this.barFilledPath.getAttribute('d').split(' ')
            let prev = document.querySelector(`.id-${id}`)
            if (prev) {
              prev.remove()
            }
            const needle = Utils.createSVGElement('line', {
              class: `id-${id} radial-gauge-needle`,
              x1: 50,
              y1: needleY,
              x2: needleCoord[needleCoord.length - 2],
              y2: needleCoord[needleCoord.length - 1],
            })

            this.rootSvg.appendChild(needle)
          }

          updateGauge(value) {
            const { min, max, startAngle, endAngle, precision, displayValue, displayNeedle } = this.options
            const percentage = Utils.getValueInPercentage(value, min, max)
            const angle = Utils.getAngle(percentage, 360 - Math.abs(startAngle - endAngle))
            const flag = angle <= 180 ? 0 : 1

            this.barFilledPath.setAttribute('d', Utils.pathString(this.radius, startAngle, angle + startAngle, flag))
            if (displayValue) {
              this.valueText.textContent = value.toFixed(precision)
            }
            if (displayNeedle) {
              this.drawNeedle()
            }
          }

          setValue(value) {
            const { min, max } = this.options
            this.options.value = Utils.normalize(value, min, max)
            this.updateGauge(this.options.value)
          }

          setValueAnimated(value, duration = 1) {
            const { value: oldValue, min, max } = this.options
            this.options.value = Utils.normalize(value, min, max)

            new Animation({
              start: oldValue,
              end: this.options.value,
              duration,
              step: (val) => this.updateGauge(val),
            }).startAnimation()
          }

          getValue() {
            return this.options.value
          }

          getRange() {
            const { min, max } = this.options
            return { min, max }
          }
        }

        return Gauge
      })
    </script>
  </head>

  <body>
    <div class="gauge210"></div>
    <div class="gauge270"></div>

    <script>
      const gauge210 = new Gauge(document.querySelector('.gauge210'), {
        id: '210',
        min: 0,
        max: 210,
        value: 100,
        ticks: 7,
        startAngle: 165,
        endAngle: 15,
        titleY: 35,
        unitsY: 41,
        needleY: 50,
        valueY: 62,
      })

      const gauge270 = new Gauge(document.querySelector('.gauge270'), {
        id: '270',
        min: 0,
        max: 270,
        value: 100,
        ticks: 9,
        startAngle: 135,
        endAngle: 45,
        titleY: 40,
        unitsY: 46,
        needleY: 60,
        valueY: 78,
      })

      setInterval(function () {
        const randomValue210 = (Math.random() * gauge210.getRange().max).toFixed(2)
        gauge210.setValueAnimated(parseFloat(randomValue210), 0.5)

        const randomValue270 = (Math.random() * gauge270.getRange().max).toFixed(2)
        gauge270.setValueAnimated(parseFloat(randomValue270), 0.5)
      }, 1000)
    </script>
  </body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?