<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)}`)
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.