<button id="button">Click Me</button>

<div id="config">
  <div class="config-wrap">
    <label
      id="grid-size-label"
      for="grid-size"
    >
      Grid Size (5)
    </label>
    <input
      type="range"
      min="1"
      max="20"
      value="5"
      id="grid-size"
      name="grid-size"
    />
  </div>
  <div class="config-wrap">
    <label
      id="grid-noise-scale-label"
      for="grid-noise-scale"
    >
      Noise Scale (0.05)
    </label>
    <input
      type="range"
      min="0.01"
      max="0.25"
      step="0.01"
      value="0.05"
      id="grid-noise-scale"
      name="grid-noise-scale"
    />
  </div>
  <div class="config-wrap">
    <label
      id="grid-color-label"
      for="grid-color"
    >
      Grid Color (#2980b9)
    </label>
    <input
      type="color"
      min="1"
      max="20"
      step="1"
      id="grid-color"
      name="grid-color"
      value="#2980b9"
    />
  </div>
</div>

<script id="c" type="text/worklet">
  // Perlin noise helper
  // Credits to https://github.com/joeiddon
  
  const perlin = {
      rand_vect: function(){
          let theta = Math.random() * 2 * Math.PI
          return {x: Math.cos(theta), y: Math.sin(theta)}
      },
      dot_prod_grid: function(x, y, vx, vy){
          let g_vect
          let d_vect = {x: x - vx, y: y - vy}
          if (this.gradients[[vx,vy]]){
              g_vect = this.gradients[[vx,vy]]
          } else {
              g_vect = this.rand_vect()
              this.gradients[[vx, vy]] = g_vect
          }
          return d_vect.x * g_vect.x + d_vect.y * g_vect.y
      },
      smootherstep: function(x){
          return 6*x**5 - 15*x**4 + 10*x**3
      },
      interp: function(x, a, b){
          return a + this.smootherstep(x) * (b-a)
      },
      seed: function(){
          this.gradients = {}
          this.memory = {}
      },
      get: function(x, y) {
          if (this.memory.hasOwnProperty([x,y]))
              return this.memory[[x,y]]
          let xf = Math.floor(x)
          let yf = Math.floor(y)
          //interpolate
          let tl = this.dot_prod_grid(x, y, xf,   yf)
          let tr = this.dot_prod_grid(x, y, xf+1, yf)
          let bl = this.dot_prod_grid(x, y, xf,   yf+1)
          let br = this.dot_prod_grid(x, y, xf+1, yf+1)
          let xt = this.interp(x-xf, tl, tr)
          let xb = this.interp(x-xf, bl, br)
          let v = this.interp(y-yf, xt, xb)
          this.memory[[x,y]] = v
          return v
      }
  }
  perlin.seed()
  
  // Map one range to another
  const mapRange = function (val, in_min, in_max, out_min, out_max) {
    return (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
  }
  
  // Worklet code
  
  const PAINTLET_NAME = 'grid'

  class CSSPaintlet {
    static get inputProperties() {
      return [
        `--${PAINTLET_NAME}-size`,
        `--${PAINTLET_NAME}-color`,
        `--${PAINTLET_NAME}-noise-scale`,
        `--${PAINTLET_NAME}-is-clicked`
      ]
    }

    paint(ctx, paintSize, props, args) {
      const gridSize = Number(props.get(`--${PAINTLET_NAME}-size`))
      const color = props.get(`--${PAINTLET_NAME}-color`)
      const noiseScale = Number(props.get(`--${PAINTLET_NAME}-noise-scale`))
      const isClicked = Number(props.get(`--${PAINTLET_NAME}-is-clicked`)) === 1

      ctx.fillStyle = color
      for (let x = 0; x < paintSize.width; x += gridSize) {
        for (let y = 0; y < paintSize.height; y += gridSize) {
          ctx.globalAlpha = mapRange(perlin.get(x * noiseScale, y * noiseScale), -1, 1, 0.5, 1)
          if (isClicked) {
            ctx.globalAlpha = ctx.globalAlpha * 0.8
          }
          ctx.fillRect(x, y, gridSize, gridSize)
        }
      }
    }
  }

  registerPaint(PAINTLET_NAME, CSSPaintlet)
</script>
* {
  margin: 0;
  padding: 0;
  font-family: Helvetica, sans-serif;
}

body {
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

#button {
  --grid-size: 5;
  --grid-color: #2980b9;
  
  padding: 2rem 6rem;
  color: white;
  font-size: 3rem;
  border: none;
  border-radius: 20px;
  
  background: paint(grid);
  background-repeat: no-repeat;

  cursor: pointer;
}

#config {
  position: fixed;
  bottom: 0;
  left: 0;
  padding: 1rem;
  background: rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(3px);
  box-sizing: border-box;
  border-top-right-radius: 24px;
}

.config-wrap {
  margin-bottom: 0.75rem;
  color: rgba(0, 0, 0, 0.8);
}

label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  margin-bottom: 0.5rem;
}
console.clear()

const paintletCode = document.getElementById('c')

;(async function() {
  // ⚠️ Handle Firefox and Safari by importing a polyfill for CSS Pain    
  if (CSS['paintWorklet'] === undefined) {
    await import('https://unpkg.com/css-paint-polyfill')
  }

  // Explicitly define our custom CSS variable
  // Make sure that the browser treats it as a number
  
  if ('registerProperty' in CSS) {
    CSS.registerProperty({
      name: '--grid-size',
      syntax: '<number>',
      inherits: false,
      initialValue: 5
    })
    CSS.registerProperty({
      name: '--grid-noise-scale',
      syntax: '<number>',
      inherits: false,
      initialValue: 0.05
    })
    CSS.registerProperty({
      name: '--grid-color',
      syntax: '<color>',
      inherits: false,
      initialValue: '#2980b9'
    })
    CSS.registerProperty({
      name: '--grid-is-clicked',
      syntax: '<number>',
      inherits: false,
      initialValue: 0
    })
  }
  
  const gridSizeInput = document.getElementById('grid-size')
  const gridScaleInput = document.getElementById('grid-noise-scale')
  const gridColorInput = document.getElementById('grid-color')
  
  const gridSizeLabel = document.getElementById('grid-size-label')
  const gridScaleLabel = document.getElementById('grid-noise-scale-label')
  const gridColorLabel = document.getElementById('grid-color-label')
  
  const workletElement = document.getElementById('button')
  
  workletElement.addEventListener('mousedown', e => {
    workletElement.style.setProperty('--grid-is-clicked', 1)
  })
  
  workletElement.addEventListener('mouseup', e => {
    workletElement.style.setProperty('--grid-is-clicked', 0)
  })
  
  gridSizeInput.addEventListener('change', e => {
    const gridSize = Number(e.target.value)
    gridSizeLabel.textContent = `Grid Size (${gridSize})`
    workletElement.style.setProperty('--grid-size', gridSize)
  })
  gridScaleInput.addEventListener('change', e => {
    const noiseScale = Number(e.target.value)
    gridScaleLabel.textContent = `Noise Scale (${noiseScale})`
    workletElement.style.setProperty(`--grid-noise-scale`, noiseScale)
  })
  gridColorInput.addEventListener('change', e => {
    const gridColor = e.target.value
    gridColorLabel.textContent = `Grid Color (${gridColor})`
    workletElement.style.setProperty('--grid-color', gridColor)
  })

  // Include our paintlet using
  registerCSSPaintlet(paintletCode.textContent)
})()

function registerCSSPaintlet (sourceCode) {  CSS.paintWorklet.addModule(`data:application/javascript;charset=utf8,${encodeURIComponent(sourceCode)}`)
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.