<!-- 
      Copyright (c) 2018  Ricardo Mendieta
      Released under the MIT license
      http://opensource.org/licenses/mit-license.php
 -->
<div class="original">参考:<q><a href="https://codepen.io/mendieta/pen/WgvENJ">Ink Cursor</a></q></div>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="800">
    <defs>
      <filter id="goo">
        <!-- ぼかし -->
        <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
        <!-- 透過度に対してコントラストをつける -->
        <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 35 -15" result="goo" />
        <feComposite in="SourceGraphic" in2="goo" operator="atop" />
      </filter>
    </defs>
  </svg>
  <div id="cursor" class="Cursor"></div>
/*
  Copyright (c) 2018  Ricardo Mendieta
  Released under the MIT license
  http://opensource.org/licenses/mit-license.php
*/

.Cursor {
  pointer-events: none;
  position: fixed;
  display: block;
  border-radius: 0;
  transform-origin: center center;
  top: 0;
  left: 0;
  z-index: 1000;
  filter: url("#goo");
  color: #000;
  
  span {
    position: absolute;
    color: #000;
    display: block;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    transform-origin: center center;
    transform: translate(-50%, -50%);
    background-color: #000;
  }
}

html,body {
  height: 100vh;
  width: 100vw;
  background-color: #fff;
  overflow: hidden;
}

svg {
  display: none;
}

.original {
  font-size: 20px;
}
View Compiled
/**
 * Copyright (c) 2018  Ricardo Mendieta
 * Released under the MIT license
 * http://opensource.org/licenses/mit-license.php
 */

const cursor = document.getElementById('cursor')
const amount = 26 //生成する円の数
const sineDots = Math.floor(amount * 0.4) //蠢かせる円の数
const width = 30 //円の大きさ
const idleTimeout = 100 //カーソルが動いてる状態から静止してうごめく状態になるまでの時間[ms]
let mousePosition = { x: 0, y: 0}
let dots: Dot[] = []
let timeoutID: number
let idle = false

class Dot {
 x = 0 // x座標
 y = 0 // y座標
 private anglespeed = 0.05 //動くスピード(可変)
 private element = document.createElement('span')
 private lockX = this.x // カーソルが止まった時のx座標
 private lockY = this.y // カーソルが止まった時のy座標
 private scale: number // 円の大きさ
 private range: number // 円が動く範囲
 private angleX = 0  // 0 ~ 2π の乱数:(後述)
 private angleY = 0 // 0 ~ 2π の乱数:(後述)

  constructor(
    private index: number, // 円のindex(0 ~ amount)
  ){
    // 円の大きさ(可変)
    // indexが大きくなる = 後に生成された円ほど小さくなる
    this.scale = 1 - 0.04 * this.index

    // 円が動く範囲(可変)
    this.range = width / 2 - width / 2 * this.scale + 2
    
    this.element.classList.add(String(this.index))
    gsap.set(this.element, { scale: this.scale });
    cursor?.appendChild(this.element)
  }

  //カーソルが止まった時に1度だけ呼び出される
  public lock() {
    this.lockX = this.x;//とまったときのx座標の位置
    this.lockY = this.y;//とまったときのy座標の位置
    this.angleX = Math.PI * 2 * Math.random();// (0 ~ 2πの乱数)。カーソルが止まった時に乱数固定
    this.angleY = Math.PI * 2 * Math.random();
  }

  //止まった円の描画
  //メインのストーカー(最大幅の円)はカーソルの位置で固定
  //それより小さいストーカーが蠢いている
  public draw() {
    //idle: うごめいているフラグ、 sinDotes: これ以下のインデックスはうごめかせない
    if (!idle || this.index <= sineDots) {//うごめいていない(とまっていない) or インデックスがsinDots以下(蠢かせない円) === うごめかせる円もうごめかせない円も描写
      gsap.set(this.element, {x: this.x, y: this.y})
    } else {//うごめいている(とまっている) and インデックスがsinDots以上(うごめかせる円)  == うごめかせるたまだけの描写
      this.angleX += this.anglespeed;//うごめく速度
      this.angleY += this.anglespeed;//うごめく速度
      // this.lockX = 0
      this.y = this.lockY + Math.sin(this.angleY) * this.range; //y方向のうごめき
      this.x = this.lockX + Math.cos(this.angleX) * this.range; //x方向のうごめき
      gsap.set(this.element, { x: this.x, y: this.y });
    }
  }
}

// 円の生成
const buildDots = () => {
  for (let i = 0; i < amount; i++) {
    let dot = new Dot(i)
    dots.push(dot)
  }
}

const onMouseMove = (e: MouseEvent)  => {
  mousePosition.x = e.clientX - width / 2
  mousePosition.y = e.clientY - width / 2
  resetIdleTimer()
}

const onTouchMove = (e: TouchEvent) => {
  mousePosition.x = e.touches[0].clientX - width / 2
  mousePosition.y = e.touches[0].clientY - width / 2
  resetIdleTimer()
}

//カーソルが動いてる状態から静止してうごめく状態にする
const startIdleTimer = () => {
  timeoutID = setTimeout(goInactive, idleTimeout);//idolTimeout[ms]後に蠢く状態にする
  idle = false;//蠢いていない
}

const resetIdleTimer= () => {
  clearTimeout(timeoutID);
  startIdleTimer();
}

//うごめくきっかけの関数
//カーソルが動いている状態 => 静止してうごめく際に1回だけ呼ばれる
const goInactive = () => {
  idle = true;//蠢いている
  for (let dot of dots) {
    dot.lock();
  }
}

// 描画更新
const positionCursor = () => {
  let x = mousePosition.x
  let y = mousePosition.y
  dots.forEach((dot, index, dots) => {
    let nextDot = dots[index + 1] || dots[0]
    dot.x = x
    dot.y = y
    dot.draw()
    //カーソルが動いている時
    // 0.4は可変
    if (!idle || index <= sineDots) {
      const dx = (nextDot.x - dot.x) * 0.4;
      const dy = (nextDot.y - dot.y) * 0.4;
      x += dx;
      y += dy;
  }
  })
}

// レンダリング
const render = () => {
  positionCursor()
  requestAnimationFrame(render)
}

// 初期化
const init =  () => {
  window.addEventListener('mousemove', onMouseMove)
  window.addEventListener('touchmove', onTouchMove)
  buildDots()
  render()
}


init()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js