<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
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.