SOURCE

console 命令行工具 X clear

                    
>
console
let stopwatch;
let cpb;
let label;
let body;
let btn;
let lapsElm = [];
let laps;
let lastLap = 0;

window.onload = () => {
    cpb = document.getElementById("cpb");
    label = document.querySelector(".label");
    laps = document.querySelector(".laps");
    body = document.querySelector("body");
    btn = document.querySelector(".container");
    stopwatch = new Stopwatch(handleUpdate);
    body.dataset.state = "idle";
}

function toggle() {
    switch (stopwatch.state) {
        case "idle":
            stopwatch.start();
            break;
        case "running":
            stopwatch.stop();
            break;
        case "stop":
            stopwatch.resume();
            break;
    }
    pressBtn();
    body.dataset.state = stopwatch.state;
}

function pressBtn() {
    btn.dataset.state = "press";
    setTimeout(() => {
        btn.dataset.state = "";
    }, 350)
}

function doReset() {
    stopwatch.reset();
    pressBtn();
    body.dataset.state = "idle";
    laps.innerHTML = "";
    lastLap = 0;
    lapsElm = [];
}

function doLap() {
    let time = stopwatch.lap();
    let delta = time - lastLap;
    lastLap = time;
    let d = document.createElement("div")
    d.classList.add("lap");
    d.time = time;
    d.delta = delta;
    d.innerHTML = `
    <div class="total">${format(parseTime(time))}</div>
    <div class="laptime">${format(parseTime(delta))}</div>
    `;
    lapsElm.push(d);
    laps.append(d)
    pressBtn();
    updateLaps();
}

function parseTime(time) {
    let ms = time % 100 + "";
    let s = Math.floor(time / 100) % 60 + "";
    let m = Math.floor(time / 6000) % 60 + "";
    return { ms, s, m };
}

function format({ m, s, ms }) {
    return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}:${ms.toString().padStart(2, "0")}`;
}

function updateLaps() {
    lapsElm.sort((a, b) => a.delta > b.delta ? 1 : -1).forEach((e, i) => {
        e.dataset.rank =
            (i + 1).toString().padStart(2, "0");
    })
}

class Stopwatch {
    constructor(cb) {
        this.time = 0; //in ms
        this.intervalHandler;
        this.onUpdate = cb;
        this.state = "idle";
    }

    tick() {
        this.time += 1;
        this.onUpdate(this.time);
    }

    start() {
        this.intervalHandler = setInterval(
            this.tick.bind(this),
            10
        )
        this.state = "running";
    }

    lap() { return this.time }

    stop() {
        clearInterval(this.intervalHandler)
        this.state = "stop";
    }

    reset() {
        this.stop();
        this.time = 0;
        this.onUpdate(this.time)
        this.state = "idle";
    }

    resume() { this.start() }
}

function handleUpdate(time) {
    let t = parseTime(time);
    label.innerText = format(t);
    cpb.setValue(t.s % 60);
}
class Counter {
    constructor(target, initCount, duration) {
        this.target = target;
        this.curCount = initCount;
        this.start;
        this.end;
        this.clock;
        this.p;
        this.delay = duration / 10;
        this.target.innerText = initCount;
    }

    setCount(count) {
        this.end = count;
        this.start = this.curCount;
        this.p = 0;

        clearInterval(this.clock);

        this.clock = setInterval(
            () => {
                this.p += 0.1;
                this.curCount = Math.round(this.p * (this.end - this.start) + this.start);
                this.target.innerText = this.curCount;

                if (this.p >= 1) {
                    clearInterval(this.clock);
                    this.target.innerText = this.end;
                    this.curCount = this.end;
                }
            },
            this.delay
        );
    }
}

class CircularProgressbar extends HTMLElement {

    constructor() {
        super();
        this.shadowDom = this.attachShadow({ mode: "open" });
    }

    validateAttributes() {
        this.angle = parseInt(this.getAttribute("angle")) || 0;
        this.size = parseInt(this.getAttribute("size")) || 200;
        this.gap = parseInt(this.getAttribute("gap")) || 0;
        this.stroke = this.getAttribute("color") || "hsl(220,75%,70%)";
        this.strokeTrack = this.getAttribute("track-color") || "#eee";
        this.strokeWidth = parseInt(this.getAttribute("width")) || 4;
        this.min = parseInt(this.getAttribute("min")) || 0;
        this.max = parseInt(this.getAttribute("max")) || 100;
        this.value = parseInt(this.getAttribute("value")) || this.min;
        this.angle += this.gap / 2 + 90;
        while (this.angle < 0)
            this.angle += 360;
        if (this.size < 100)
            this.size = 100;
        this.gap %= 360;

        if (this.max < this.min) {
            this.max = this.min;
        }

        if (this.value < this.min || this.value > this.max) {
            this.value = this.min;
        }
    }

    connectedCallback() {
        this.validateAttributes();

        let wrapper = document.createElement("div");
        wrapper.setAttribute("class", "wrapper");

        let svgNS = "http://www.w3.org/2000/svg";
        let svg = document.createElementNS(svgNS, "svg");

        svg.style.width = (this.size + this.strokeWidth / 2) + "px";
        svg.style.height = (this.size + this.strokeWidth / 2) + "px";

        let rad = this.size * 0.5 - 8;
        this.radius = rad;
        let meter = document.createElementNS(svgNS, "circle");
        meter.style.stroke = this.strokeTrack;
        meter.setAttribute("class", "meter");
        meter.setAttribute("cx", "50%");
        meter.setAttribute("cy", "50%");
        meter.setAttribute("r", this.radius);

        let arc = document.createElementNS(svgNS, "circle");
        arc.setAttribute("class", "arc");
        arc.style.stroke = this.stroke;
        arc.setAttribute("cx", "50%");
        arc.setAttribute("cy", "50%");
        arc.setAttribute("r", this.radius);
        this.arc = arc;

        let display = document.createElement("div");
        display.classList.add("display");
        this.counter = new Counter(display, this.value, 800);

        let arcFraction = (360 - this.gap) / 360;
        this.arcLength = arcFraction * (2 * Math.PI * this.radius)
        arc.style.strokeDasharray = meter.style.strokeDasharray = this.arcLength + "px , " + (2 * Math.PI * this.radius) + "px";
        arc.style.strokeDashoffset = 0;

        svg.style.transform = `rotate(${this.angle}deg)`;

        let styleElement = document.createElement("style");
        styleElement.innerHTML = `
            .wrapper {
                position:relative;
                width  : ${this.offsetWidth + "px"};
                height : ${this.offsetHeight + "px"};
                min-width  : ${2.5 * this.radius}px ;
                min-height : ${2.5 * this.radius}px ;
                display:flex;
                align-items:center;
                justify-content:center;
            }
            .arc,.meter {
                fill: none ;
                stroke-linecap : round ;
                stroke-width:${this.strokeWidth}px;
                transition: all 1000ms linear;
            }

            .arc {
                stroke-width : ${this.strokeWidth + 1}px ;
            }
            .display {
                display:none;
                position:absolute;
                top:50%;
                left:50%;
                transform :translate(-50%,-50%);
                font-size:4rem;
                color:${this.stroke};
                font-weight:900;
            }
        `;
        svg.appendChild(meter);
        svg.appendChild(arc);
        wrapper.appendChild(svg);
        wrapper.appendChild(display);
        this.shadowDom.appendChild(styleElement);
        this.shadowDom.appendChild(wrapper);

        this.setValue(this.value);
    }

    setValue(value) {
        value = Math.min(this.max, Math.max(this.min, value));
        let v = Math.abs((value - this.min) / (this.max - this.min));

        v = (v > 1) ? 1 : v;
        v = (v < 0) ? 0 : v;
        let val = (this.arcLength * (1 - v));
        this.arc.style.strokeDashoffset = val + "px";
    }
}
customElements.define(
    "circular-progressbar",
    CircularProgressbar
);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CSS</title>
    <!-- <link rel="stylesheet" href="14.css" />
    <script src="14.js" defer></script> -->
  </head>
  <body data-state="idle"></body>
  <div class="wrapper">
    <div class="timer">
      <div class="container" onclick="toggle()">
        <circular-progressbar
          id="cpb"
          min="0"
          max="60"
          value="0"
          angle="180"
          gap="0"
          size="256"
          width="16"
          color="#b224ef"
          track-color="white"
        >
        </circular-progressbar>
        <div class="label">00:00:00</div>
        <div class="controls">
          <div class="playpause"></div>
        </div>
      </div>
    </div>
    <div class="actions">
      <button onclick="doReset()">清零</button>
      <button class="lapbtn" onclick="doLap()">标记</button>
    </div>
    <div class="laps"></div>
  </div>
</html>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
html {
  font-family: sans-serif;
}
.wrapper {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  color: #fff;
  background-image: linear-gradient(to top, #b224ef 0%, #7579ff 100%);
  overflow-y: auto;
}
.timer {
  padding: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.container {
  position: relative;
}
.container::befor {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 40%;
  height: 40%;
  z-index: 1;
  background-color: #fff;
}
.container::after {
  content: "";
  position: absolute;
  right: 20%;
  top: 10%;
  transform: translate(50%, -50%) rotate(35deg);
  width: 3rem;
  height: 1rem;
  border-radius: 500px;
  z-index: 1;
  background-color: #fff;
}
.container[data-state="press"]::after {
  animation: press 200ms ease-in;
}
@keyframes press {
  0% {
    transform: translate(50%, -50%) rotate(35deg);
  }
  50%,
  80% {
    transform: translate(30%, -20%) rotate(35deg);
  }
}
[data-state="running"] .container::after {
  background-color: #fff;
}
.actions {
  padding: 1rem;
  display: flex;
  justify-content: center;
  transition: all 200ms ease-out;
}
.actions > * + * {
  margin-left: 1rem;
}
.label {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  z-index: 2;
  font-size: 2rem;
  transition: all 200ms ease-out;
}
.playpause {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3rem;
  height: 3rem;
  transform: translate(-50%, 40%) scale(0.5);
  z-index: 15;
  overflow: hidden;
  clip-path: polygon(10% 0%, 90% 50%, 90% 50%, 10% 100%);
  transition: all 200ms ease-out;
  cursor: pointer;
}
.playpause::before,
.playpause::after {
  content: "";
  position: absolute;
  width: 1.5rem;
  height: 100%;
  top: 0;
  left: 0;
  background: #fff;
  pointer-events: none;
  transition: all 200ms ease-out;
}
.playpause::after {
  right: 0;
  left: unset;
}

[data-state="idle"] .playpause {
  transform: translate(-50%, -50%);
}

[data-state="running"] .playpause {
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}

[data-state="running"] .playpause::before {
  transform: translate(-0.35rem);
}

[data-state="running"] .playpause::after {
  transform: translate(0.35rem);
}

[data-state="idle"] .label {
  opacity: 0;
}

[data-state="stop"] .lapbtn {
  transform: scale(0.95);
  pointer-events: none;
  opacity: 0.9;
}

.laps {
  padding: 1rem;
}

.laps > * + * {
  margin-top: 0.5rem;
}

[data-state="idle"] .actions {
  opacity: 0;
  pointer-events: none;
  transform: scale(0.5);
}

@keyframes fly-up {
  0% {
    transform: translateY(5rem);
    opacity: 0;
  }
  100% {
    transform: translate(0);
    opacity: 1;
  }
}

button {
  display: inline-block;
  padding: 1rem 1rem;
  border-radius: 4px;
  border: none;
  background-color: #fff;
  font-weight: bold;
  color: hsl(220 20% 60%);
  transition: all 200ms ease-out;
  width: 8rem;
}

button:active {
  animation: pressbtn 100ms ease-out alternate;
}

@keyframes pressbtn {
  to {
    transform: scale(0.9);
  }
}

.lap {
  --r: "00";
  position: relative;
  padding: 1.5rem;
  background-color: #fff;
  border-radius: 8px;
  animation: fly-up 200ms ease-out;
  font-family: monospace;
  padding-left: 5.5rem;
  max-width: 24rem;
  margin-left: auto;
  margin-right: auto;
}

.lap::before {
  content: attr(data-rank);
  position: absolute;
  left: 1.25rem;
  top: 50%;
  width: 3rem;
  height: 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  transform: translate(0, -50%);
  font-weight: 700;
  color: hsl(220 10% 40%);
}

.laptime {
  font-size: 1.25rem;
  font-weight: 500;
  color: hsl(220 10% 40%);
}

.total {
  font-size: 0.8rem;
  color: hsl(220 10% 40%);
}

.lap[data-rank="01"]::before {
  color: hsl(220 80% 50%);
}