Last active
July 20, 2025 21:22
-
-
Save nikfox3/e270d4fec67ad5ec194e1d0ac06b101d to your computer and use it in GitHub Desktop.
Sticker Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<svg height="0" width="0"> | |
<defs> | |
<filter id=pointLight> | |
<feGaussianBlur stdDeviation="1" result="blur" /> | |
<feSpecularLighting result="spec" in="blur" specularExponent="100" specularConstant="0.1" lightingColor="white"> | |
<fePointLight x=100 y=100 z="300" /> | |
</feSpecularLighting> | |
<feComposite in="spec" in2="SourceGraphic" operator="screen" result="lit" /> | |
<feComposite in="lit" in2="SourceAlpha" operator="in" /> | |
</filter> | |
<filter id=pointLightFlipped> | |
<feGaussianBlur stdDeviation="10" result="blur" /> | |
<feSpecularLighting result="spec" in="blur" specularExponent="100" specularConstant="0.7" lightingColor="white"> | |
<fePointLight x=100 y=100 z="300" id="fePointLightFlipped" /> | |
</feSpecularLighting> | |
<feComposite in="spec" in2="SourceGraphic" operator="screen" result="lit" /> | |
<feComposite in="lit" in2="SourceAlpha" operator="in" /> | |
</filter> | |
<filter id="dropShadow"> | |
<feDropShadow dx="2" dy="4" stdDeviation="3" flood-color="black" flood-opacity="0.6" /> | |
</filter> | |
<!-- STROKE EFFECT (1/2) --> | |
<!-- Uncomment the outerStroke filter to add a stroke effect --> | |
<!-- Set the stroke size by changing the radius value in feMorphology --> | |
<!-- For strokes to work, you need to update expandAndFill filter too --> | |
<!-- <filter id="outerStroke"> | |
<feMorphology operator="dilate" radius="10" in="SourceAlpha" result="expanded" /> | |
<feColorMatrix in="expanded" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" | |
result="whiteExpanded" /> | |
<feComposite operator="xor" in="whiteExpanded" in2="SourceAlpha" result="outerStroke" /> | |
<feComposite operator="over" in="SourceGraphic" in2="outerStroke" /> | |
</filter> --> | |
<filter id="expandAndFill"> | |
<feOffset dx="0" dy="0" in="SourceAlpha" result="shape" /> | |
<!-- STROKE EFFECT (2/2) --> | |
<!-- Uncomment the feMorphology filter to add a stroke effect --> | |
<!-- <feMorphology operator="dilate" radius="10" in="SourceAlpha" result="shape" /> --> | |
<feFlood flood-color="rgb(179, 179, 179)" result="flood" /> | |
<feComposite operator="in" in="flood" in2="shape" /> | |
</filter> | |
</defs> | |
</svg> | |
<style> | |
</style> | |
<main style="width: 100dvw; height: 100dvh; display: flex; justify-content: center; align-items: center;"> | |
<div class="draggable"> | |
<div class="sticker-container"> | |
<div class="sticker-main"> | |
<div class="sticker-lighting"> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png" | |
class="sticker-image" | |
draggable="false" | |
oncontextmenu="return false" /> | |
<div class="shadow"> | |
<div class="flap"> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png" | |
class="shadow-image" | |
draggable="false" | |
oncontextmenu="return false" /> | |
</div> | |
</div> | |
<div class="flap"> | |
<div class="flap-lighting"> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png" | |
class="flap-image" | |
draggable="false" | |
oncontextmenu="return false" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</main> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const pointLight = document.querySelector("fePointLight"); | |
const pointLightFlipped = document.getElementById("fePointLightFlipped"); | |
const stickerContainer = document.querySelector(".sticker-container"); | |
const draggable = document.querySelector(".draggable"); | |
let isDragging = false; | |
let dragOffsetX = 0; | |
let dragOffsetY = 0; | |
function getContainingBlockOffset(element) { | |
let containingBlock = element.offsetParent || document.documentElement; | |
const containingBlockRect = containingBlock.getBoundingClientRect(); | |
return { | |
left: containingBlockRect.left, | |
top: containingBlockRect.top | |
}; | |
} | |
function updateLightPosition(mouseX, mouseY) { | |
const rect = stickerContainer.getBoundingClientRect(); | |
const relativeX = mouseX - rect.left; | |
const relativeY = mouseY - rect.top; | |
pointLight.setAttribute("x", relativeX); | |
pointLight.setAttribute("y", relativeY); | |
pointLightFlipped.setAttribute("x", relativeX); | |
pointLightFlipped.setAttribute("y", rect.height - relativeY); | |
} | |
function startDrag(e) { | |
isDragging = true; | |
const rect = draggable.getBoundingClientRect(); | |
const containingBlockOffset = getContainingBlockOffset(draggable); | |
dragOffsetX = e.clientX - rect.left; | |
dragOffsetY = e.clientY - rect.top; | |
draggable.style.left = rect.left - containingBlockOffset.left + "px"; | |
draggable.style.top = rect.top - containingBlockOffset.top + "px"; | |
} | |
function updateDragPosition(e) { | |
if (isDragging) { | |
const containingBlockOffset = getContainingBlockOffset(draggable); | |
draggable.style.left = | |
e.clientX - dragOffsetX - containingBlockOffset.left + "px"; | |
draggable.style.top = | |
e.clientY - dragOffsetY - containingBlockOffset.top + "px"; | |
} | |
} | |
function stopDrag() { | |
isDragging = false; | |
} | |
draggable.addEventListener("mousedown", startDrag); | |
document.addEventListener("mouseup", stopDrag); | |
document.addEventListener("mousemove", (e) => { | |
updateLightPosition(e.clientX, e.clientY); | |
updateDragPosition(e); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { | |
/* CONFIGURATION */ | |
--sticker-rotate: 10deg; | |
/** If the image is clipped after rotation, | |
/* increase this value! **/ | |
--sticker-p: 50px; | |
--sticker-peelback-hover: 10%; | |
--sticker-peelback-active: 50%; | |
--sticker-peel-easing: 2s | |
linear( | |
0, | |
0.002 0.4%, | |
0.008 0.9%, | |
0.02 1.4%, | |
0.035 1.9%, | |
0.055 2.4%, | |
0.083 3%, | |
0.11 3.5%, | |
0.146 4.1%, | |
0.214 5.1%, | |
0.297 6.2%, | |
0.624 10.2%, | |
0.756 11.9%, | |
0.821 12.8%, | |
0.874 13.6%, | |
0.93 14.5%, | |
0.975 15.3%, | |
1.016 16.1%, | |
1.053 16.9%, | |
1.085 17.7%, | |
1.116 18.6%, | |
1.139 19.4%, | |
1.16 20.3%, | |
1.176 21.2%, | |
1.187 22.1%, | |
1.195 23.2%, | |
1.197 24.4%, | |
1.193 25.6%, | |
1.183 26.9%, | |
1.17 28.1%, | |
1.153 29.4%, | |
1.055 35.6%, | |
1.031 37.3%, | |
1.012 38.8%, | |
0.994 40.6%, | |
0.98 42.3%, | |
0.97 44.1%, | |
0.964 45.9%, | |
0.961 48.3%, | |
0.964 51.1%, | |
0.97 53.7%, | |
0.997 62.7%, | |
1.003 66%, | |
1.007 69.3%, | |
1.007 74.4%, | |
1 89.2%, | |
1 | |
); | |
--sticker-peel-hover-easing: 1s | |
linear( | |
0, | |
0.008 1.1%, | |
0.031 2.2%, | |
0.129 4.8%, | |
0.257 7.2%, | |
0.671 14.2%, | |
0.789 16.5%, | |
0.881 18.6%, | |
0.957 20.7%, | |
1.019 22.9%, | |
1.063 25.1%, | |
1.094 27.4%, | |
1.114 30.7%, | |
1.112 34.5%, | |
1.018 49.9%, | |
0.99 59.1%, | |
1 | |
); | |
/* HELPERS */ | |
--sticker-start: calc(-1 * var(--sticker-p)); | |
--sticker-end: calc(100% + var(--sticker-p)); | |
} | |
body { | |
padding: 0; | |
margin: 0; | |
background-image: linear-gradient(to bottom, #302f31, #1a1a1a); | |
} | |
.sticker-container { | |
position: relative; | |
} | |
.sticker-container * { | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
-webkit-touch-callout: none; | |
-webkit-tap-highlight-color: transparent; | |
} | |
.sticker-main { | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-start), | |
var(--sticker-end) var(--sticker-start), | |
var(--sticker-end) var(--sticker-end), | |
var(--sticker-start) var(--sticker-end) | |
); | |
transition: clip-path var(--sticker-peel-hover-easing); | |
filter: url(#dropShadow); | |
} | |
.sticker-lighting { | |
filter: url(#pointLight); | |
} | |
.sticker-container:hover .sticker-main { | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-peelback-hover), | |
var(--sticker-end) var(--sticker-peelback-hover), | |
var(--sticker-end) var(--sticker-end), | |
var(--sticker-start) var(--sticker-end) | |
); | |
transition: clip-path var(--sticker-peel-hover-easing); | |
} | |
.sticker-container:active .sticker-main { | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-peelback-active), | |
var(--sticker-end) var(--sticker-peelback-active), | |
var(--sticker-end) var(--sticker-end), | |
var(--sticker-start) var(--sticker-end) | |
); | |
transition: clip-path var(--sticker-peel-easing); | |
} | |
.sticker-image { | |
transform: rotate(var(--sticker-rotate)); | |
filter: url(#outerStroke); | |
} | |
.flap { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
left: 0; | |
top: calc(-100% - var(--sticker-p) - var(--sticker-p)); | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-start), | |
var(--sticker-end) var(--sticker-start), | |
var(--sticker-end) var(--sticker-start), | |
var(--sticker-start) var(--sticker-start) | |
); | |
transform: scaleY(-1); | |
transition: all var(--sticker-peel-hover-easing); | |
} | |
.sticker-container:hover .flap { | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-start), | |
var(--sticker-end) var(--sticker-start), | |
var(--sticker-end) var(--sticker-peelback-hover), | |
var(--sticker-start) var(--sticker-peelback-hover) | |
); | |
top: calc(-100% + 2 * var(--sticker-peelback-hover) - 1px); | |
transition: all var(--sticker-peel-hover-easing); | |
} | |
.sticker-container:active .flap { | |
clip-path: polygon( | |
var(--sticker-start) var(--sticker-start), | |
var(--sticker-end) var(--sticker-start), | |
var(--sticker-end) var(--sticker-peelback-active), | |
var(--sticker-start) var(--sticker-peelback-active) | |
); | |
top: calc(-100% + 2 * var(--sticker-peelback-active) - 1px); | |
transition: all var(--sticker-peel-easing); | |
} | |
.flap-lighting { | |
filter: url(#pointLightFlipped); | |
} | |
.flap-image, | |
.shadow-image { | |
transform: rotate(var(--sticker-rotate)); | |
filter: url(#expandAndFill); | |
} | |
.shadow { | |
position: absolute; | |
top: 1rem; | |
left: 0.5rem; | |
width: 100%; | |
height: 100%; | |
filter: brightness(0) blur(8px); | |
opacity: 0.4; | |
} | |
.sticker-main, | |
.flap { | |
will-change: clip-path, transform; | |
} | |
.draggable { | |
position: absolute; | |
cursor: grab; | |
} | |
.draggable:active { | |
cursor: grabbing; | |
} | |
.sticker-image, | |
.shadow-image, | |
.flap-image { | |
width: 200px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment