- const MODES = ['normal', 'screen', 'multiply', 'overlay', 'hard-light', 'soft-light', 'color-dodge', 'color-burn'];
//- limits within which the alpha may vary (in %)
- const ALPHA_MIN = 20, ALPHA_MAX = 100;
- let alpha_val = .5*(ALPHA_MIN + ALPHA_MAX), m = 5;
//- pass alpha and blend mode to CSS as custom properties
body(style=`--alpha: ${alpha_val}; --mode: ${MODES[m]}`)
// contains nothing but filter => functionally same as a style elem
// zero its dimensions and hide it from screen readers
svg(width='0' height='0' aria-hidden='true')
// restrict filter region to input box
// sRGB is what we normally want (but not default)
filter#noise(x='0' y='0' width='1' height='1'
color-interpolation-filters='sRGB')
// generate fine noise
feTurbulence(type='fractalNoise' baseFrequency='.713' numOctaves='4')
// fully desaturate noise
feColorMatrix(type='saturate' values='0')
// control alpha of noise layer and blend mode
form.controls
// instead of fieldset for better styling options
div.alpha(role='group' aria-label='Control panel for setting the noise layer alpha.' style=`--min: ${ALPHA_MIN}; --max: ${ALPHA_MAX}`)
label(for='alpha') Noise alpha
// div wrapper for layout purposes :(
div.range
input#alpha(type='range' min=ALPHA_MIN max=ALPHA_MAX value=alpha_val)
output(for='alpha')
div.blend(role='group' aria-label='Control panel for setting the blend mode used to blend the noise layer with the original gradient.')
label(for='mode') Blend mode
select#mode
button: selectedcontent
- MODES.forEach((c, i) => {
option(value=c label=c selected=m === i) #{c}
- })
// visual result section with 3 panels
section.results
.card.bands: span original gradient (banding)
.card.noise: span noise over white/ black
.card.layer: span blend noise with gradient
View Compiled
// for slider styling
$track-h: 6px;
$track-r: .5*$track-h;
$thumb-d: 1.25em;
$thumb-r: .5*$thumb-d;
// why mixins? see
// https://css-tricks.com/sliding-nightmare-understanding-range-input/
@mixin track() {
height: $track-h;
border-radius: $track-r;
background:
linear-gradient(90deg,
var(--hlc) var(--pos),
currentcolor 0)
}
@mixin thumb($f: 0) {
/* Firefox only */
@if $f == 0 { border: none }
/* WebKit only */
@else { margin-top: calc(#{$track-r} + -1*#{$thumb-r}) }
// can't use aspect-ratio here
width: $thumb-d;
height: $thumb-d;
border-radius: 50%;
background: var(--hlc);
cursor: ew-resize
}
/* set for controls */
* { font: inherit }
html, body, div {
display: grid;
grid-gap: .5em
}
/* take it out of document flow */
svg[height='0'][aria-hidden='true'] { position: fixed }
html {
/* stretch across at least 1 viewport */
min-height: 100%;
/* my attempt at a pretty light and dark palette
* of course it didn't come out pretty,
* what in Odin's name was I thinking? */
color-scheme: light dark;
background: light-dark(#e88c7d, #467ec3);
color: light-dark(#283c55, #eebba0)
}
body {
/* controls take up as much vertical space as needed,
* allow result to fill out the remaining vertical space */
/* restrict it all horizontally */
grid-template: 1fr auto/ min(100%, 75em);
/* middle align as necessary */
place-content: center;
/* vary font size, but within certain limits */
font: clamp(1em, 2vw, 1.5em)/ 1.125 cairo, sans-serif
}
/* flexbox everything! */
form, section, [role='group'] {
display: flex;
flex-wrap: wrap;
gap: .5em
}
form, span { background: light-dark(#eebba0, #022450) }
form {
--hlc: light-dark(#71093b, #eaaa34);
--fgc: light-dark(#eaaa34, #71093b);
/* I don't even remember why I added this
* or why removing it breaks things ¯\_(ツ)_/¯ */
container-type: inline-size;
justify-content: center;
/* separate alpha & blend mode controls
* with a bit more space */
gap: 1em;
z-index: 1; /* Firefox */
padding: .5em
}
[role='group'] {
/* middle align everything, looks ugly otherwise */
align-items: center;
justify-content: center
}
/* visual interaction hint */
input, select { cursor: pointer }
/* what I Satan's name am I doing? no idea ¯\_(ツ)_/¯ */
/* https://css-tricks.com/four-layouts-for-the-price-of-one/ */
.alpha, .range { flex: 1 1 30ch }
/* compute a few basic things */
.alpha {
--dif: (var(--alpha) - var(--min));
--rng: (var(--max) - var(--min));
--prg: var(--dif)/var(--rng)
}
/* to be able to use cqw for its children */
.range { container-type: inline-size }
output, select {
font-family: roboto mono, monospace
}
[type='range'] {
/* allow styling in WebKit browsers */
&, &::-webkit-slider-thumb { appearance: none }
/* default sucks huge donkey dicks */
background: #0000;
&::-webkit-slider-runnable-track { @include track }
&::-moz-range-track { @include track }
/* Firefox supports this, yo! */
&::slider-track { @include track }
/* different styles for different browser engines
* thanks to mixin params */
&::-webkit-slider-thumb { @include thumb(1) }
&::-moz-range-thumb { @include thumb }
/* Firefox supports this, yo! */
&::slider-thumb { @include thumb }
&, & + output {
/* middle of thumb position */
--pos: calc(#{$thumb-r} + var(--prg)*(100cqw - #{$thumb-d}));
/* stack them up
* https://mastodon.social/@anatudor/113774781963966085 */
grid-area: 1/ 1
}
& + output {
/* left align */
justify-self: start;
/* give text inside room to breathe */
padding: .5em;
/* middle align with thumb, move above slider */
translate: calc(var(--pos) - 50%) -100%;
background: var(--hlc);
color: var(--fgc);
/* hack to display numeric var in content
* (which requites string) */
counter-reset: a var(--alpha);
&::after { content: counter(a) '%' }
}
}
.blend {
flex: 0 0 min-content;
min-width: calc(13ch + 2em);
/* oh, wait, this is why removing parent containr broke things */
/* also boolean logic!
* https://mastodon.social/@anatudor/114606670001499403 */
@container ((width > 200px) and (width < 388px)) or (width > 580px) {
flex-basis: max-content
}
}
/* custom style select, hello!
* https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select */
select,
::picker(select) {
appearance: base-select;
border: solid 1px #0000;
min-width: 13ch;
border-radius: 0;
background: light-dark(#eaac8b, #355070) padding-box
}
::picker(select) {
overflow: hidden;
height: max-height
}
option {
white-space: nowrap;
&:is(:hover, :focus) {
background: light-dark(#990b52, #cb8b15);
color: var(--fgc);
}
&:checked {
background: var(--hlc);
font-weight: bold
}
}
/* visual result */
section {
/* wide screen case, along x axis */
--dir: 0;
grid-row: 1;
/* narrow screen case, along y axis */
@container (max-width: 720px) {
--dir: 1;
flex-direction: column
}
}
.card {
/* white/ black split angle */
--ang: calc((2 - var(--dir))*90deg);
flex: 1 1 30%;
min-height: calc((3 - 2*var(--dir))*8em);
/* cut out any filter edge weirdness */
clip-path: inset(1px);
/* no blending for first two panels */
&:nth-child(-n + 2) { --mode: normal }
/* noise layer fully transparent for first panel */
&:nth-child(1) { --alpha: 0 }
/* white black split instead of example gradient for panel two */
&:nth-child(2) { --slist: #fff 50%, #000 0 }
&::before, &::after {
grid-area: 1/ 1;
content: ''
}
/* panel background gradient */
&::before {
background:
linear-gradient(var(--ang), var(--slist, #a9613a, #1e1816));
}
/* noise layer */
&::after {
opacity: calc(.01*var(--alpha));
filter: url(#noise);
mix-blend-mode: var(--mode)
}
/* panel label */
span {
place-self: start;
grid-area: 1/ 1;
z-index: 2;
margin: .5em;
padding: .5em;
font-family: roboto condensed, sans-serif
}
}
View Compiled
/* all the JS does is update one custom property
* when changing the slider or select value */
addEventListener('input', e => {
// get control that was modified
let _t = e.target;
// update corresponding custom property on body
document.body.style.setProperty(`--${_t.id}`, _t.value)
})
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.