SOURCE

console 命令行工具 X clear

                    
>
console
let box = document.querySelector('.box')

// 获取 canvas 元素和上下文
var line_canvas = document.getElementById("circleCanvas");
var line_ctx = line_canvas.getContext("2d");

var dot_canvas = document.getElementById("dotCanvas");
var dot_ctx = dot_canvas.getContext("2d");

let num = 1           // 数量
let first_radius = 50   // 第一个的半径
let interval = 10       // 间距
// 绘图中心点
let center_x = line_canvas.width / 2
let center_y = line_canvas.height
// 小球的线速度
var linearSpeed = 1; // 可以根据需要调整线速度的大小

let circle_list = []
let dot_list = []


// 随机生成颜色数组
function generateRainbowColors(numColors, saturation = 1, value = 1) {
    function HSVtoRGB(h, s, v) {
        let r, g, b, i, f, p, q, t;
        if (s === 0) {
            r = g = b = v;
        } else {
            h /= 60;
            i = Math.floor(h);
            f = h - i;
            p = v * (1 - s);
            q = v * (1 - s * f);
            t = v * (1 - s * (1 - f));
            switch (i % 6) {
                case 0:
                    r = v; g = t; b = p;
                    break;
                case 1:
                    r = q; g = v; b = p;
                    break;
                case 2:
                    r = p; g = v; b = t;
                    break;
                case 3:
                    r = p; g = q; b = v;
                    break;
                case 4:
                    r = t; g = p; b = v;
                    break;
                case 5:
                    r = v; g = p; b = q;
                    break;
            }
        }
        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }
    const rainbowColors = [];
    for (let i = 0; i < numColors; i++) {
        // 计算当前色相值,从红色(0°)到紫色(360°)
        const hue = (i / numColors) * 360;
        // 使用指定的饱和度和亮度
        const [r, g, b] = HSVtoRGB(hue, saturation, value);
        // 转换成十六进制字符串
        const color = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
        rainbowColors.push(color);
    }
    return rainbowColors;
}

// 混合两种颜色
function mixColor(color1, color2, weight) {
    var d2h = function (d) { return d.toString(16).padStart(2, '0'); }; // 将十进制值转为十六进制
    var h2d = function (h) { return parseInt(h, 16); }; // 将十六进制值转为十进制

    // 解析颜色
    var color = "#";
    for (var i = 1; i < 7; i += 2) {
        var v1 = h2d(color1.substr(i, 2));
        var v2 = h2d(color2.substr(i, 2));
        // 混合两种颜色的分量
        var val = d2h(Math.floor(v2 + (v1 - v2) * weight));
        color += val;
    }
    return color;
}

// 绘制半圆
function drawCircle() {
    circle_list.forEach(option => {
        line_ctx.beginPath();
        line_ctx.arc(center_x, center_y, option.radius, Math.PI, 0);
        line_ctx.lineWidth = option.line_width; // 设置线宽来控制圆环的宽度
        line_ctx.strokeStyle = option.color; // 设置圆环的颜色
        line_ctx.stroke();
    })
}
// 绘制小球
function drawDot() {
    dot_ctx.clearRect(0, 0, dot_canvas.width, dot_canvas.height);
    // 绘制每个小球
    dot_list.forEach(function (ball) {
        var x = center_x - ball.radius * Math.cos(ball.angle);
        var y = center_y - ball.radius * Math.sin(ball.angle);

        // 计算当前的角度所占比例
        var weight = ball.angle / Math.PI; // 从 0 到 1 的权重

        // 根据方向更新角度
        if (ball.direction) {
            ball.angle += ball.angularSpeed;
        } else {
            ball.angle -= ball.angularSpeed;
        }

        // 当圆点达到180度或0度时改变运动方向
        if (ball.angle >= Math.PI || ball.angle <= 0) {
            ball.direction = !ball.direction;
        }

        // 如果是逆时针方向 (direction == false),则从指定颜色混合到白色
        // 如果是顺时针方向 (direction == true),则从白色混合到指定颜色
        var color = ball.direction
            ? mixColor(ball.color, '#FFFFFF', weight)
            : mixColor('#FFFFFF', ball.color, weight);

        // 绘制小球
        dot_ctx.beginPath();
        dot_ctx.arc(x, y, 5, 0, 2 * Math.PI); // 小球的大小固定为半径 5
        dot_ctx.fillStyle = color;
        dot_ctx.fill();
    });
    requestAnimationFrame(drawDot);
}

function init() {
    let colors = generateRainbowColors(num, 0.75, 0.8)
    new Array(num).fill({}).forEach((item, index) => {
        let radius = first_radius + index * interval
        circle_list.push({
            line_width: 2,
            radius,
            color: colors[index],
        })
        dot_list.push({
            angularSpeed: linearSpeed / radius,
            radius,
            angle: 0,
            color: colors[index],
            direction: true
        })
    })
    drawCircle()
    drawDot()
}

init()
<div class="relative box">
	<canvas class="absolute" width="600" height="400" id="circleCanvas"></canvas>
	<canvas class="absolute" width="600" height="400" id="dotCanvas"></canvas>
</div>
.relative{
    position: relative
}
.absolute{
    position: absolute
}

.box{
    width: 600px;
    height: 400px;
    background-color: #282828;
}