- let n_rows = 8, n_cols = 21, n_dots = 7;
- let m = n_rows*n_cols;

style
  - for(let j = 0; j < n_cols; j++)
    | .cell:nth-child(#{n_cols}n + #{j + 1}) { --i: #{j} }
  - for(let j = 0; j < n_rows; j++)
    | .cell:nth-child(n + #{n_cols*j + 1}) { --j: #{j} }
  - for(let j = 0; j < n_dots; j++)
    | .dot:nth-child(#{j + 1}) { --idx: #{j} }
.grid(style=`--n-rows: ${n_rows}; --n-cols: ${n_cols}; --n-dots: ${n_dots}`)
  - for(let i = 0; i < m; i++)
    .cell
      - for(let j = 0; j < n_dots; j++)
        .dot
View Compiled
$d: .5em;
$r: .5*$d;
$p: 5%;
$t: 1s;

body, div { display: grid }

body { background: #262626 }

.grid {
  grid-gap: $d 2*$d;
  grid-template: 
    repeat(var(--n-rows), calc(var(--n-dots)*#{$d})) /
    repeat(var(--n-cols), $d);
  place-self: center
}

.dot {
  --k: calc(var(--idx)/var(--n-dots));
  --mj: calc(.5*(var(--n-rows) - 1));
  --abs-j: max(var(--mj) - var(--j), var(--j) - var(--mj));
  --mi: calc(.5*(var(--n-cols) - 1));
  --abs-i: max(var(--mi) - var(--i), var(--i) - var(--mi));
  grid-area: 1/ 1;
  align-self: start;
  padding: $r;
  border-radius: 50%;
  background: hsl(calc(var(--k)*360), 80%, 65%);
  mix-blend-mode: screen;
  animation: osc $t ease-in-out infinite alternate;
  animation-delay: 
    calc((var(--abs-j)/var(--mj) + 
          var(--abs-i)/var(--mi) + 
          var(--k) - 3)*#{$t})
}

@keyframes osc {
  0%, #{$p} { transform: none }
  #{100% - $p}, 100% { transform: translatey(calc((var(--n-dots) - 1)*#{$d})) }
}
View Compiled

External CSS

  1. https://codepen.io/thebabydino/pen/evPbxv.scss

External JavaScript

  1. https://codepen.io/thebabydino/pen/evPbxv.js