<input type="checkbox" id="toggle" hidden />
<label for="toggle" data-app-elm="toggle">
<svg viewBox="0 0 24 24">
<circle cx="14" cy="6" r="2" />
<line x1="4" y1="6" x2="12" y2="6" />
<line x1="16" y1="6" x2="20" y2="6" />
<circle cx="8" cy="12" r="2" />
<line x1="4" y1="12" x2="6" y2="12" />
<line x1="10" y1="12" x2="20" y2="12" />
<circle cx="17" cy="18" r="2" />
<line x1="4" y1="18" x2="15" y2="18" />
<line x1="19" y1="18" x2="20" y2="18" />
</svg>
<svg viewBox="0 0 24 24">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</label>
<svg data-app-elm="svg" id="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000" style="background-color:hsl(248, 50%, 25%)">
<g id="g"></g>
</svg>
<main>
<form id="app" data-app="dotring">
<details open>
<summary>dots & rings</summary>
<div>
<label>
number of rings
<input type="range" id="numRings" min="1" max="30" data-random />
</label>
<label>
dot size
<input type="range" id="dotSize" min="1" max="30" data-random />
</label>
<label>
dots per ring
<input type="range" id="dotsPerRing" min="1" max="30" data-random />
</label>
<label>
spread
<input type="range" id="spread" min="1" max="50" data-random />
</label>
<label data-app-elm="checkbox">
<input type="checkbox" id="randomRadius" data-random />
<span>random radius</span>
</label>
<label data-app-elm="checkbox">
<input type="checkbox" id="randomDotSize" data-random />
<span>random dot-size</span>
</label>
</div>
</details>
<details>
<summary>color ranges</summary>
<div>
<label>
hue min
<input type="range" id="hueMin" min="0" max="360" data-random />
</label>
<label>
hue max
<input type="range" id="hueMax" min="0" max="360" data-random />
</label>
<label>
saturation min
<input type="range" id="satMin" min="0" max="100" data-suffix="%" data-random />
</label>
<label>
saturation max
<input type="range" id="satMax" min="0" max="100" data-suffix="%" data-random />
</label>
<label>
lightness min
<input type="range" id="lightMin" min="0" max="100" data-suffix="%" data-random />
</label>
<label>
lightness max
<input type="range" id="lightMax" min="0" max="100" data-suffix="%" data-random />
</label>
<label>
custom color-array:
<input type="text" id="colorArray" placeholder="#FFF|white|hsl(0,0%,100%)" />
</label>
</div>
</details>
<details>
<summary>background / canvas</summary>
<div>
<label>
hue
<input type="range" id="hueBg" min="0" max="360" data-random data-attr />
</label>
<label>
saturation
<input type="range" id="satBg" min="0" max="100" data-suffix="%" data-random data-attr />
</label>
<label>
lightness
<input type="range" id="lightBg" min="0" max="100" data-suffix="%" data-random data-attr />
</label>
<label>
canvas y:x ratio
<input type="range" id="canvasRatio" min="25" max="150" value="100" />
</label>
<label>
rotate
<input type="range" min="0" max="360" value="0" id="rotate" data-attr />
</label>
<label>
scale-x
<input type="range" min="0.01" max="3" step="0.01" value="1" id="scaleX" data-attr />
</label>
<label>
scale-y
<input type="range" min="0.01" max="3" step="0.01" value="1" id="scaleY" data-attr />
</label>
<label>
translate-x
<input type="range" min="-1000" max="1000" value="0" id="translateX" data-attr />
</label>
<label>
translate-y
<input type="range" min="-1000" max="1000" value="0" id="translateY" data-attr />
</label>
</div>
</details>
<details>
<summary>export</summary>
<div>
<fieldset>
<legend>file format</legend>
<label data-app-elm="radio">
<input type="radio" name="fileFormat" value="svg" data-ignore>
<span>svg</span>
</label>
<label data-app-elm="radio">
<input type="radio" name="fileFormat" value="" data-ignore checked>
<span>png</span>
</label>
<label data-app-elm="radio">
<input type="radio" name="fileFormat" value="jpg" data-ignore>
<span>jpg</span>
</label>
<label data-app-elm="radio">
<input type="radio" name="fileFormat" value="webp" data-ignore>
<span>webp</span>
</label>
</fieldset>
<button type="button" onclick="saveFile(svg, app.elements.fileFormat.value)">Save To Image</button>
</div>
</details>
<br />
<button type="button" onclick="randomPreset()">Random!</button>
</form>
</main>
* {
box-sizing: border-box;
margin: unset;
}
html {
block-size: 100%;
inline-size: 100%;
}
body {
--app-w: 20rem;
--bgc: hsla(200, 30%, 85%, 0.95);
--bg-w: 100%;
background-color: var(--bgc);
min-block-size: 100%;
min-inline-size: 100%;
}
[data-app] {
--accent: hsl(200, 50%, 50%);
--accent-bg: hsl(200, 30%, 55%);
--border: hsl(200, 30%, 30%);
--bdrs: 0.15rem;
--gap: 1rem;
--rng-h: 2rem;
background: var(--bgc);
border-inline-start: 2px solid var(--border);
bottom: 0;
font-family: ui-monospace, monospace;
height: 100vh;
left: var(--app-l, 100%);
overflow-y: auto;
padding-inline: var(--gap);
position: fixed;
right: 0;
top: 0;
transition: left 0.5s cubic-bezier(.35, .92, 1, 1);
width: var(--app-w);
z-index: 1;
}
[data-app] button {
background-color: var(--accent-bg);
border: 1px solid var(--border);
box-shadow: 5px 5px 0 0 var(--border);
font-family: inherit;
padding: calc(var(--gap) / 2) var(--gap);
}
button[data-app-elm="preset"] {
background-color: hsl(200, 30%, 20%);
border: 0;
border-radius: 0.75rem;
box-shadow: none;
color: hsl(200, 30%, 95%);
font-family: inherit;
font-size: x-small;
margin: 0 0.25rem .5rem 0;
padding: 0.25rem 0.75rem
}
[data-app-elm="checkbox"],
[data-app-elm="radio"] {
display: block;
font-family: ui-monospace, monospace;
margin-block-end: var(--gap);
}
[data-app-elm="checkbox"] span,
[data-app-elm="radio"] span {
align-items: center;
display: flex;
position: relative;
}
[data-app-elm="checkbox"] span::before,
[data-app-elm="radio"] span::before {
background-color: var(--accent-bg);
border: 2px solid var(--border);
border-radius:50%;
content: '';
display: inline-block;
height: 1.5rem;
margin-inline-end: 0.5rem;
width: 1.5rem;
}
[data-app-elm="checkbox"] input,
[data-app-elm="radio"] input {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
left: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
[data-app-elm] input:checked + span::before {
background-color: var(--border);
box-shadow: inset 0 0 0 6px var(--accent-bg);
}
[data-app-elm="checkbox"] span::before {
border-radius: 0.25rem;
}
[data-app-elm="svg"] {
transition: width 0.5s cubic-bezier(.35, .92, 1, 1);
width: var(--bg-w);
}
[data-app-elm="toggle"] {
background-color: hsl(200, 15%, 15%);
border-color: hsl(200, 15%, 95%);
border-style: solid;
border-width: 0 1px 1px 0;
height: 44px;
position: absolute;
width: 44px;
}
[data-app-elm="toggle"] svg {
fill: none;
position: absolute;
stroke: hsl(200, 15%, 95%);
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1;
transition: opacity 1s cubic-bezier(.35, .92, 1, 1);
}
[data-app-elm="toggle"] svg:last-of-type {
opacity: 0;
}
[data-app] fieldset {
border: 1px solid var(--accent-bg);
border-radius: var(--bdrs);
margin-block-end: var(--gap);
padding: var(--gap);
}
[data-app] label {
display: block;
position: relative;
}
[data-app] label::after {
content: attr(data-value);
display: inline-block;
font-size: x-small;
position: absolute;
right: 0;
top: 0.5em;
}
[data-app] summary {
border-bottom: 2px dashed var(--accent-bg);
cursor: pointer;
padding-block: var(--gap);
user-select: none;
}
[data-app] summary + div {
border-bottom: 2px dashed var(--accent-bg);
padding-block: var(--gap);
}
[data-app] [type="range"] {
--rng-bdrs: .5rem;
--rng-bgi: linear-gradient(to right, var(--accent-bg), var(--accent-bg));
--rng-h: 0.5rem;
--rng-m: var(--gap) 0;
--rng-thumb-bdrs: 50%;
--rng-thumb-bgc: var(--accent);
--rng-thumb-bxsh: inset 0 0 0 0.125rem var(--border);
--rng-thumb-bxsh--focus: inset 0 0 0 0.125rem var(--border), 0 0 0 0.125rem rgba(255, 255, 255, 0.8);
--rng-thumb-h: 2rem;
--rng-thumb-w: 2rem;
background-image: var(--rng-bgi, inherit);
border-radius: var(--rng-bdrs, 0);
font-family: inherit;
height: var(--rng-h);
margin: var(--rng-m, 0);
outline: 0.25rem solid transparent;
position: relative;
touch-action: none;
width: 100%;
}
[data-app] [type="range"][max="360"] {
--s: 60%;
--rng-bgi: linear-gradient(to right,
hsla(0, var(--s), 50%, 0.8),
hsla(30, var(--s), 50%, 0.8),
hsla(60, var(--s), 50%, 0.8),
hsla(90, var(--s), 50%, 0.8),
hsla(120, var(--s), 50%, 0.8),
hsla(150, var(--s), 50%, 0.8),
hsla(180, var(--s), 50%, 0.8),
hsla(210, var(--s), 50%, 0.8),
hsla(240, var(--s), 50%, 0.8),
hsla(270, var(--s), 50%, 0.8),
hsla(300, var(--s), 50%, 0.8),
hsla(330, var(--s), 50%, 0.8),
hsla(360, var(--s), 50%, 0.8)
);
}
[data-app] [type="range"]::-moz-range-thumb {
background-color: var(--rng-thumb-bgc);
border-radius: var(--rng-thumb-bdrs);
box-shadow: var(--rng-thumb-bxsh);
color: #000;
cursor: ew-resize;
height: var(--rng-thumb-h);
margin-top: calc(0px - ((var(--rng-thumb-h) - var(--rng-h)) / 2));
position: relative;
width: var(--rng-thumb-w);
}
[data-app] [type="range"]::-webkit-slider-thumb {
background-color: var(--rng-thumb-bgc);
border-radius: var(--rng-thumb-bdrs);
box-shadow: var(--rng-thumb-bxsh);
cursor: ew-resize;
height: var(--rng-thumb-h);
margin-top: calc(0px - ((var(--rng-thumb-h) - var(--rng-h)) / 2));
position: relative;
width: var(--rng-thumb-w);
}
[data-app] [type="range"]:focus-visible::-webkit-slider-thumb {
box-shadow: var(--rng-thumb-bxsh--focus);
}
[data-app] [type="range"]::-moz-range-track {
background: transparent;
background-size: 100%;
height: var(--rng-h);
}
[data-app] [type="range"]::-webkit-slider-runnable-track {
background: transparent;
background-size: 100%;
height: var(--rng-h);
}
[data-app] [type="range"],
[data-app] [type="range"]::-webkit-slider-runnable-track,
[data-app] [type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
}
[data-app] [type="text"] {
border: 1px solid var(--border);
border-radius: var(--bdrs);
font-family: inherit;
margin-block-end: var(--gap);
margin-block-start: calc(var(--gap) / 4);
padding: calc(var(--gap) / 2);
width: 100%;
}
/* STATE */
#toggle:checked ~ [data-app] { --app-l: 25vw; }
#toggle:checked + [data-app-elm="toggle"] svg:first-of-type { opacity: 0; }
#toggle:checked + [data-app-elm="toggle"] svg:last-of-type { opacity: 1; }
#toggle:checked ~ [data-app-elm="svg"] {
--bg-w: calc(100% - var(--app-w));
}
#toggle:checked ~ main [data-app] {
--app-l: calc(100% - var(--app-w));
}
const apps = {
dotring: () => {
/* Returns an array of points (`number`) placed on a circle with `radius` */
const coords = (number, arr = []) => {
const frags = 360 / number;
for (let i = 0; i <= number; i++) {
arr.push((frags / 180) * i * Math.PI);
}
return arr;
}
const colors = colorArray.value.split('|');
const useColorArray = colors.length > 1;
const fill = () => useColorArray ? colors[R(colors.length)] : `hsl(${R(hueMax.valueAsNumber, hueMin.valueAsNumber)}, ${R(satMax.valueAsNumber, satMin.valueAsNumber)}%, ${R(lightMax.valueAsNumber, lightMin.valueAsNumber)}%)`;
let s = `<circle cx="500" cy="500" r="${dotSize.valueAsNumber}" fill="${fill()}" />`;
for (let i = 1; i <= numRings.valueAsNumber; i++ ) {
const r = randomRadius.checked ? R(500,1) : spread.valueAsNumber * i;
const theta = coords(dotsPerRing.valueAsNumber * i);
for (let j = 0; j < theta.length; j++) {
const x = 500 - Math.round(r * (Math.cos(theta[j])));
const y = 500 - Math.round(r * (Math.sin(theta[j])));
s+= `<circle cx="${x}" cy="${y}" r="${randomDotSize.checked ? R(35,2) : dotSize.valueAsNumber}" fill="${fill()}" />`
}
}
g.innerHTML = s;
}
}
function render(input) {
if (input?.type === 'range') {
input.parentNode.dataset.value = `${input.valueAsNumber}${input.dataset.suffix||''}`;
if (input.name) document.body.style.setProperty(input.name, `${input.valueAsNumber}${input.dataset.suffix||''}`)
}
if (input?.hasAttribute('data-ignore')) return;
if (input?.hasAttribute('data-attr')) {
setBG();
g.setAttribute('transform', `rotate(${rotate.valueAsNumber}, 500, ${canvasRatio.valueAsNumber * 5}) scale(${scaleX.valueAsNumber}, ${scaleY.valueAsNumber}) translate(${translateX.valueAsNumber}, ${translateY.valueAsNumber})`);
return;
}
svg.setAttribute('viewBox', `0 0 1000 ${canvasRatio.valueAsNumber * 10}`);
apps[app.dataset.app]();
}
/**
* @function loadPreset
* @description Loads a preset, renders preview
* @param {Object} preset
*/
function loadPreset(preset) {
Object.entries(preset).forEach(entry => {
const [key, value] = [...entry];
if (app.elements[key]?.type === 'checkbox') {
app.elements[key].checked = value === 1;
}
else {
app.elements[key].value = value;
app.elements[key].parentNode.dataset.value = value;
}
});
render();
}
/**
* @function R
* @description returns a random number between max and min
* @param {Number} max
* @param {Number} [min]
* @param {Boolean} [f]
*/
function R(max, min = 0, f = true) {
return f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;
};
/**
* @function randomPreset
* @description Creates a random preset
*/
function randomPreset() {
app.reset();
[...app.elements].forEach(input => {
if (input.hasAttribute('data-random')) {
if (input.type === 'checkbox') {
input.checked = R(10, 0) > 5;
}
if (input.type === 'range') {
input.value = R(input.max-0, input.min-0);
input.parentNode.dataset.value = `${input.value}${input.dataset.suffix||''}`;
}
}
});
setBG();
render();
}
/**
* @function saveFile
* @description Exports canvas to either svg, png, jpg or webp
* @param {Node} svg
* @param {String} [ext]
*/
function saveFile(svg, ext = 'png') {
const download = (href, name) => {
const L = document.createElement('a');
L.download = name;
L.style.opacity = "0";
document.body.append(L);
L.href = href;
L.click();
L.remove()
}
const {width, height} = svg.getBBox();
let clone = svg.outerHTML;
const blob = new Blob([clone],{type:'image/svg+xml;charset=utf-8'});
const format = { png: '', jpg: 'image/jpg', webp: 'image/webp' };
if (ext === 'svg') {
download(URL.createObjectURL(blob), 'image.svg');
return;
}
const img = new Image();
img.onload = () => {
const C = document.createElement('canvas');
C.width = width;
C.height = height;
const X = C.getContext('2d');
X.drawImage(img, 0, 0, width, height);
download(C.toDataURL(format[ext], 1), `image.${ext}`)
};
img.src = URL.createObjectURL(blob);
}
/**
* @function setBG
* @description Sets canvas-background
*/
function setBG() {
svg.setAttribute('style', `background-color: hsl(${hueBg.valueAsNumber}, ${satBg.valueAsNumber}%, ${lightBg.valueAsNumber}%)`);
}
/**
* @function toggle
* @description toggle a menu-group
* @param {Event} event
*/
function toggle(event) {
summary.forEach(node => {
if (node !== event.target) node.parentNode.open = false;
});
}
/* Init */
const summary = app.querySelectorAll('summary');
summary.forEach(node => node.addEventListener('click', toggle));
app.addEventListener('input', (event) => { if (event.target) render(event.target); });
const preset = {
numRings: 12,
dotSize: 12,
dotsPerRing: 7,
spread: 36,
randomRadius: 0,
randomDotSize: 0,
hueMin: 25,
hueMax: 50,
satMin: 50,
satMax: 90,
lightMin: 60,
lightMax: 90,
hueBg: 248,
satBg: 50,
lightBg: 25
}
loadPreset(preset);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.