<div class="grid">
<header>
<h1>Paper Snowflake Maker</h1>
<p class="intro">Drag the handles to generate a unique snowflake.</p>
</header>
<div class="container">
<div class="handle-group">
<div class="handle-group__inner">
<div class="handle" data-index="0" data-x="--x1" data-y="--y1" data-x-drag="true"></div>
<div class="handle" data-index="1" data-x="--x2" data-y="--y2" data-x-drag="true"></div>
<div class="handle" data-index="2" data-x="--x3" data-y="--y3"></div>
<div class="handle" data-index="3" data-x="--rx1a" data-y="--ry1a"></div>
<div class="handle" data-index="4" data-x="--rx1b" data-y="--ry1b" data-x-drag="true"></div>
<div class="handle" data-index="5" data-x="--rx1c" data-y="--ry1c"></div>
<div class="handle" data-index="6" data-x="--centerX1" data-y="--centerY1"></div>
<div class="handle" data-index="7" data-x="--centerX2" data-y="--centerY2"></div>
<div class="handle" data-index="8" data-x="--lx1a" data-y="--ly1a"></div>
<div class="handle" data-index="9" data-x="--lx1b" data-y="--ly1b" data-x-drag="true"></div>
<div class="handle" data-index="10" data-x="--lx1c" data-y="--ly1c"></div>
<div class="handle" data-index="11" data-x="--lx2a" data-y="--ly2a"></div>
<div class="handle" data-index="12" data-x="--lx2b" data-y="--ly2b" data-x-drag="true"></div>
<div class="handle" data-index="13" data-x="--lx2c" data-y="--ly2c"></div>
</div>
</div>
<div class="snowflake">
<div class="triangle" style="--i: 0"></div>
<div class="triangle" style="--i: 1"></div>
<div class="triangle" style="--i: 2"></div>
<div class="triangle" style="--i: 3"></div>
<div class="triangle" style="--i: 4"></div>
<div class="triangle" style="--i: 5"></div>
<div class="triangle" style="--i: 6"></div>
<div class="triangle" style="--i: 7"></div>
<div class="triangle" style="--i: 8"></div>
<div class="triangle" style="--i: 9"></div>
<div class="triangle" style="--i: 10"></div>
<div class="triangle" style="--i: 11"></div>
</div>
</div>
<div class="result-wrapper">
<h2>Clip path for single segment:</h2>
<div class="result">
<code data-path-result></code>
</div>
<button data-btn>Hide guides</button>
</div>
</div>
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700");
* {
box-sizing: border-box;
}
:root {
--c1: rgba(250, 250, 250, 1);
--c2: rgba(227, 227, 230, 1);
}
body {
font-family: Montserrat, sans-serif;
margin: 0;
padding: 1rem;
max-width: 100%;
overflow-x: hidden;
display: flex;
justify-content: center;
background-color: rgba(21, 21, 31, 1);
color: var(--c1);
@media (min-width: 45rem) {
font-size: 1.1rem;
}
}
.intro {
@media (min-width: 45rem) {
font-size: 1.4rem;
}
}
h1 {
@media (min-width: 45rem) {
font-size: 3.5rem;
margin: 0 0 1rem;
}
}
h2 {
font-size: 1.5rem;
margin: 0 0 1rem;
}
.result {
line-height: 1.4;
}
.grid {
display: grid;
gap: 1rem;
max-width: 80rem;
@media (min-width: 60rem) {
grid-template-columns: auto minmax(0, 1fr);
gap: 2rem 3rem;
}
}
header {
grid-column: 1 / -1;
text-align: center;
@media (min-width: 60rem) {
padding: 3rem 0;
}
}
.container {
--height: min(90vw, 40rem);
--s: 12;
--angle: calc(360deg / var(--s));
--y3: 10%; /* Clip y position */
--a: calc(100% - var(--y3)); /* Adjacent side length */
--x3: calc(var(--o) * (100% - var(--y3)));
--x1: 15%;
--y1: 0;
--x2: 20%;
--y2: 17%;
--bg: repeating-conic-gradient(var(--c1), var(--c1), var(--angle), var(--c2) var(--angle), var(--c2) calc(var(--angle) * 2));
/* Cutout sections */
--ry1a: 20%;
--ry1c: 62%;
/* "free" values */
--rx1b: 28%;
--ry1b: 25%;
/* Calculate x values for right side */
--rx1a: calc(var(--o) * (100% - var(--ry1a)));
--rx1c: calc(var(--o) * (100% - var(--ry1c)));
--r1: var(--rx1a) var(--ry1a), var(--rx1b) var(--ry1b), var(--rx1c) var(--ry1c);
--centerY1: 85%;
--centerX1: calc(var(--o) * (100% - var(--centerY1)));
--centerX2: 0;
--centerY2: 90%;
--center: var(--centerX1) var(--centerY1), var(--centerX2) var(--centerY2);
/* Left side values should start at x = 0 */
--ly1a: 70%;
--ly1c: 50%;
/* These are "free" values, can be anything */
--lx1b: 13%;
--ly1b: 45%;
--l1: 0 var(--ly1a), var(--lx1b) var(--ly1b), 0 var(--ly1c);
--ly2a: 40%;
--ly2c: 15%;
/* These are "free" values, can be anything */
--lx2b: 9%;
--ly2b: 30%;
--l2: 0 var(--ly2a), var(--lx2b) var(--ly2b), 0 var(--ly2c);
position: relative;
width: var(--height);
height: var(--height);
opacity: 0;
}
.snowflake {
&::before {
--c1: rgba(205, 205, 255, 0.05);
--c2: rgba(200, 200, 255, 0.12);
content: '';
position: absolute;
inset: 0;
background: repeating-conic-gradient(var(--c1), var(--c1), var(--angle), var(--c2) var(--angle), var(--c2) calc(var(--angle) * 2));
}
}
.handle-group {
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 0;
}
.handle-group__inner {
width: 100%;
height: 100%;
position: relative;
}
.handle {
width: 20px;
height: 20px;
position: absolute;
background-color: transparentize(darkorchid, 0.65);
border-radius: 50%;
border: 2px solid darkorchid;
z-index: 1;
transform: translate3d(-50%, -50%, 0);
opacity: 0.9;
&.is-active {
background-color: deeppink;
border: 2px solid deeppink;
opacity: 0.8;
}
}
.triangle {
--clip: polygon(0 0, var(--x1) var(--y1), var(--x2) var(--y2), var(--x3) var(--y3), var(--r1), var(--center), var(--l1), var(--l2));
--bg: repeating-conic-gradient(from 0deg at 0 100%, white, rgba(200, 200, 200, 1) var(--angle));
width: 50%;
height: 50%;
position: absolute;
top: 0;
left: 50%;
background: var(--bg);
-webkit-clip-path: var(--clip);
clip-path: var(--clip);
transform: rotate(calc(var(--i) * var(--angle)));
transform-origin: bottom left;
&:nth-child(even) {
transform: rotateY(180deg) rotate(calc((var(--i) - 1) * var(--angle)));
background: repeating-conic-gradient(from 0deg at 0 100%, rgba(200, 200, 200, 1), white var(--angle));
}
&:first-child {
background: rgba(180, 180, 205, 1);
}
}
.result {
display: grid;
grid-template-columns: auto 1fr;
gap: 0 0.25rem;
padding: 1rem;
background-color: rgba(12, 12, 20, 1);
border-radius: 0.4rem;
margin-bottom: 1rem;
.current {
color: deeppink;
}
@media (min-width: 60rem) {
margin-bottom: 2rem;
}
}
.handles-hidden {
filter: drop-shadow(1rem 1rem 1rem rgba(0, 0, 0, 0.9));
.snowflake::before {
display: none;
}
.triangle:first-child {
background: var(--bg);
}
}
button {
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
background: rgba(204, 211, 219, 1);
cursor: pointer;
font-family: inherit;
font-size: 1.1rem;
font-weight: 700;
min-width: 10rem;
transition: background 200ms;
&:hover,
&:focus {
background: rgba(155, 155, 170, 1);
}
&.is-active {
background: deeppink;
&:hover,
&:focus {
background: rgba(155, 155, 170, 1);
}
}
}
View Compiled
const el = document.querySelector('.container')
const snowflake = document.querySelector('.snowflake')
const segments = getComputedStyle(el).getPropertyValue('--s')
const switcher = document.querySelector('[data-btn]')
const angle = 360 / parseFloat(segments)
const handles = [...document.querySelectorAll('.handle')]
const pathResult = document.querySelector('[data-path-result]')
const triangles = [...document.querySelectorAll('.triangle')]
const button = document.querySelector('[data-btn]')
const getTanFromDegrees = (angle) => {
const tanFromDegrees = Math.tan(angle * Math.PI / 180)
return tanFromDegrees.toFixed(3)
}
const initialize = () => {
el.style.setProperty('--o', getTanFromDegrees(angle))
el.style.opacity = 1
}
/* Set initial positions for handles */
const positionHandles = () => {
handles.forEach((handle) => {
const propX = getComputedStyle(el).getPropertyValue(handle.dataset.x)
const propY = getComputedStyle(el).getPropertyValue(handle.dataset.y)
handle.style.left = propX
handle.style.top = propY
})
}
const a = el.clientHeight / 2
const boundWidth = getTanFromDegrees(angle) * a + 10
const calculateCustomProperty = (relativeValue, parentValue) => {
const value = (relativeValue / parentValue) * 100
const valueAsPercentage = Math.round(value)
return `${valueAsPercentage}%`
}
const setActiveState = (target) => {
handles.forEach((handle) => {
handle.classList.remove('is-active')
})
target.classList.add('is-active')
}
const setClipPathValue = (activeIndex) => {
const values = handles.map((handle, index) => {
const triangle = triangles[0]
const { x, y } = handle.dataset
const valueX = getComputedStyle(triangle).getPropertyValue(x)
const valueY = getComputedStyle(triangle).getPropertyValue(y)
const className = index == parseInt(activeIndex) ? 'current' : ''
if (valueX) {
return `<span class="${className}">${valueX} ${valueY}</span>`
} else {
return `<span class="${className}">0 ${valueY}</span>`
}
}).join(', ')
return pathResult.innerHTML = `clip-path: polygon(0 0, ${values});`
}
setClipPathValue()
const onDragHandle = (pointermove) => {
const { target, x, y } = pointermove
/* Prevent error if target is dragged out of bounds */
if (!target.parentElement) return
const parentPosX = target.parentElement.getBoundingClientRect().left
const parentPosY = target.parentElement.getBoundingClientRect().top
const parentWidth = target.parentElement.clientWidth
const parentHeight = target.parentElement.clientHeight
const relativeX = x - parentPosX
const relativeY = y - parentPosY
const propertyX = target.dataset.x
const propertyY = target.dataset.y
const x1 = calculateCustomProperty(relativeX, parentWidth)
const y1 = calculateCustomProperty(relativeY, parentHeight)
/* Set custom properties */
el.style.setProperty(propertyY, y1)
if (target.dataset.xDrag) {
el.style.setProperty(propertyX, x1)
}
setClipPathValue(target.dataset.index)
}
Draggable.create('.handle', {
bounds: { top: -12, left: -12, width: boundWidth + 24, height: a + 24 },
onDrag: onDragHandle,
edgeResistance: 1,
onPress: ({ target }) => {
setActiveState(target)
setClipPathValue(target.dataset.index)
}
})
const toggleHandles = () => {
const handleGroup = document.querySelector('.handle-group')
el.classList.toggle('handles-hidden')
button.classList.toggle('is-active')
if (handleGroup.hidden) {
button.innerText = 'Hide guides'
} else {
button.innerText = 'Show guides'
}
handleGroup.hidden = !handleGroup.hidden
}
initialize()
positionHandles()
button.addEventListener('click', toggleHandles)
This Pen doesn't use any external CSS resources.