<canvas id="main-score"></canvas>
const createNewCanvas = (width = 256, height = 1): HTMLCanvasElement => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  return canvas
}

const getLinearGradient = (
  canvas: HTMLCanvasElement,
  colors: [number, string][]
): CanvasGradient | undefined => {
  const linearGradient = canvas.getContext('2d')?.createLinearGradient(0, 0, canvas.width, 0)
  colors.forEach(([position, color]) => {
    linearGradient?.addColorStop(position, color)
  })
  return linearGradient
}

const getPalatteCanvasImageData = (colors: [number, string][]): Uint8ClampedArray | undefined => {
  const palatteCanvas: HTMLCanvasElement = createNewCanvas()
  const ctx = palatteCanvas.getContext('2d')
  if (!ctx) return
  ctx.fillStyle = getLinearGradient(palatteCanvas, colors) || ''
  ctx.fillRect(0, 0, palatteCanvas.width, palatteCanvas.height)
  return ctx.getImageData(0, 0, palatteCanvas.width, palatteCanvas.height).data
}

class Palette {
  private imageData: Uint8ClampedArray | undefined

  constructor(
    colors: [number, string][] = [
      [0, '#000'],
      [1, '#fff'],
    ]
  ) {
    this.imageData = getPalatteCanvasImageData(colors)
  }

  pickColor(pos: number): Uint8ClampedArray | undefined {
    return this.imageData?.slice(pos * 4, pos * 4 + 4)
  }
}

interface Config {
  width: number
  height: number
  startAngle: number
  endAngle: number
  bgEndAngle: number
  interval: number
  total: number
  lineWidth: number
  bgColor: string
  r: number
  totalFramesLength: number
  angleChangeForFrame: number
  duration: number
  clockwise: boolean
  colors?: [number, string][]
  canvasId: string
  palette: any
  ctx: CanvasRenderingContext2D
}

const defaultConfig: Partial<Config> = {
  width: 100,
  height: 100,
  startAngle: 180,
  endAngle: 180,
  bgEndAngle: 365,
  interval: 5,
  total: 2000,
  lineWidth: 15,
  bgColor: '#cccc',
  r: 100,
  colors: [
    [0, '#009'],
    [1, '#f00'],
  ],
}

const precision = 2
const angleDistance1 = 1
const angleDistance2 = 1.6

const angleToRadian = (angle: number): number => (angle / 180) * Math.PI

const setCanvasStyle = (option: Config, canvas: HTMLCanvasElement): void => {
  canvas.width = option.width
  canvas.height = option.height
}

const setStrokeStyle = (ctx: CanvasRenderingContext2D, color: number[]): void => {
  const [r, g, b, alpha] = color
  ctx.strokeStyle = `rgb(${r},${g},${b},${alpha / 255})`
}

const setLineStyle = (option: Config): void => {
  const ctx = option.ctx
  ctx.lineWidth = option.lineWidth
  ctx.globalAlpha = 1
  ctx.lineCap = 'round'
}

const drawArc = (option: Config, currentFrameIndex: number): void => {
  const { startAngle, bgEndAngle, width, height, ctx, bgColor, r, clockwise, angleChangeForFrame } =
    option
  let endAngle: number = startAngle + currentFrameIndex * angleChangeForFrame
  if (Math.abs(endAngle - option.endAngle) < Math.abs(angleChangeForFrame)) {
    endAngle = option.endAngle
  }
  ctx.clearRect(0, 0, width, height)

  ctx.beginPath()
  ctx.strokeStyle = bgColor
  const d: number = angleToRadian(startAngle)
  const c: number = angleToRadian(bgEndAngle)
  const x: number = width >> 1
  const y: number = height >> 1
  ctx.arc(x, y, r, d, c, !clockwise)
  ctx.stroke()
  ctx.closePath()

  splitArc(option, x, y, r, startAngle, endAngle)
}

const splitArc = (
  option: Config,
  x: number,
  y: number,
  r: number,
  start: number,
  end: number
): void => {
  const { ctx, clockwise, palette } = option
  const splitTotal: number = Math.abs(end - start)
  const j: number = clockwise ? 1 : -1
  for (let i = 0; i < splitTotal; ) {
    const color = palette.pickColor(Math.round((i * 255) / splitTotal))
    ctx.beginPath()
    setStrokeStyle(ctx, color)
    const _start: number = start + i * j
    let _end: number = _start + j
    const mod: number = Math.abs(_start) % 90
    if (mod > 20 || mod < 70) {
      _end = _start + angleDistance2 * j
      i += angleDistance2
    } else {
      i += angleDistance1
    }
    ctx.arc(x, y, r, angleToRadian(_start), angleToRadian(_end), !clockwise)
    ctx.stroke()
  }
}

const startDraw = (option: Config): void => {
  let currentFrameIndex = 0
  let drawFlag = true
  let unLockLoop = (): void => {
    drawFlag = true
    setTimeout(unLockLoop, option.interval)
  }
  unLockLoop()
  requestAnimationFrame(progress)

  function progress(): void {
    if (drawFlag) {
      drawFlag = false
      currentFrameIndex += 1
      drawArc(option, currentFrameIndex)
      if (currentFrameIndex >= option?.totalFramesLength) {
        unLockLoop = () => {
          drawFlag = false
          unLockLoop = () => {}
        }
        return
      }
    }
    requestAnimationFrame(progress)
  }
}
const buildConfig = (options: Config, ctx: CanvasRenderingContext2D): Config => {
  const option = {
    ...defaultConfig,
    ...options,
    ctx,
    palette: new Palette(options.colors || defaultConfig.colors),
  }
  option.width *= precision
  option.height *= precision
  option.lineWidth *= precision
  option.r *= precision
  return {
    ...option,
    totalFramesLength: option.duration / option.interval,
    angleChangeForFrame:
      ((option.endAngle - option.startAngle) * option.interval) / option.duration,
    clockwise: option.endAngle - option.startAngle > 0,
  }
}

const CircleProgress = (options: Partial<Config>): void => {
  const canvas: HTMLCanvasElement | null = document.querySelector(`#${options.canvasId}`)
  if (!canvas || !options) return
  const ctx = canvas.getContext('2d')
  if (!ctx) return
  const option: Config = buildConfig(options as Config, ctx)
  canvas.style.transform = 'scale(0.5)'
  canvas.style.width = `${option.width}px`
  canvas.style.height = `${option.height}px`
  // @ts-ignore
  canvas.style['transform-origin'] = 'left top'
  setCanvasStyle(option, canvas)
  setLineStyle(option)
  startDraw(option)
}
 


CircleProgress({
    canvasId: 'main-score',
    r: 118, // 默认100,半径
    width: 252,
    height: 252, // 画布高,默认500
    startAngle: 165, // 起始角度,默认180
    bgEndAngle: 380, // 起始角度,默认180
    endAngle: 300, // 终止角度,必须传
    interval: 16, // 帧间隔时间,默认5ms
    lineWidth: 16, // 描边线宽度,默认15
    bgColor: 'rgba(218, 228, 247, .3)', // 背景色,默认#cccc
    colors: [
      [0, '#7DE4BC'],
      [1, '#33C68A'],
    ],
    duration: 1000, // 动画时间,默认2000
  })
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.