502 lines
14 KiB
HTML
502 lines
14 KiB
HTML
<html>
|
|
<main id="app">
|
|
|
|
<div class="card">
|
|
<span class="glow"></span>
|
|
</div>
|
|
|
|
</main>
|
|
</html>
|
|
<style>
|
|
:root {
|
|
|
|
/* vars */
|
|
--glow-sens: 30;
|
|
--card-bg: linear-gradient(8deg,var(--dark) 75%, color-mix(in hsl, var(--dark), white 2.5%) 75.5%);
|
|
--blend: soft-light;
|
|
--glow-blend: plus-lighter;
|
|
--glow-color: 40deg 80% 80%;
|
|
--glow-boost: 0%;
|
|
|
|
}
|
|
|
|
.light .card {
|
|
--card-bg: linear-gradient(8deg,color-mix(in hsl, hsl(260, 25%, 95%), var(--dark) 2.5%) 75%, hsl(260, 25%, 95%) 75.5%);
|
|
--blend: darken;
|
|
--glow-blend: luminosity;
|
|
--glow-color: 280deg 90% 95%;
|
|
--glow-boost: 15%;
|
|
--fg: black;
|
|
color: var(--fg);
|
|
}
|
|
|
|
body.light {
|
|
|
|
background-image:
|
|
linear-gradient(
|
|
180deg,
|
|
hsl(var(--h), 8%, 58%),
|
|
hsl(var(--h), 15%, 42%)
|
|
);
|
|
}
|
|
|
|
|
|
.card {
|
|
|
|
--pads: 40px;
|
|
--color-sens: calc(var(--glow-sens) + 20);
|
|
--pointer-°: 45deg;
|
|
|
|
position: relative;
|
|
width: clamp(320px, calc(100svw - calc(var(--pads) * 2)), 600px);
|
|
height: calc(100svh - calc(var(--pads) * 2));
|
|
max-height: 600px;
|
|
border-radius: 1.768em;
|
|
isolation: isolate;
|
|
transform: translate3d(0, 0, 0.01px);
|
|
display: grid;
|
|
border: 1px solid rgb(255 255 255 / 25%);
|
|
background: var(--card-bg);
|
|
background-repeat: no-repeat;
|
|
|
|
&::before,
|
|
&::after,
|
|
& > .glow {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: inherit;
|
|
transition: opacity 0.25s ease-out;
|
|
z-index: -1;
|
|
}
|
|
|
|
&:not(:hover):not(.animating) {
|
|
&::before,
|
|
&::after,
|
|
& > .glow {
|
|
opacity: 0;
|
|
transition: opacity 0.75s ease-in-out;
|
|
}
|
|
}
|
|
|
|
&::before {
|
|
|
|
/* mesh gradient border */
|
|
|
|
border: 1px solid transparent;
|
|
|
|
background:
|
|
linear-gradient(var(--card-bg) 0 100%) padding-box,
|
|
linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,
|
|
radial-gradient(at 80% 55%, hsla(268,100%,76%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 69% 34%, hsla(349,100%,74%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 8% 6%, hsla(136,100%,78%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 41% 38%, hsla(192,100%,64%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 86% 85%, hsla(186,100%,74%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 82% 18%, hsla(52,100%,65%,1) 0px, transparent 50%) border-box,
|
|
radial-gradient(at 51% 4%, hsla(12,100%,72%,1) 0px, transparent 50%) border-box,
|
|
linear-gradient(#c299ff 0 100%) border-box;
|
|
|
|
/* opacity increases as pointer gets near edge */
|
|
opacity: calc((var(--pointer-d) - var(--color-sens)) / (100 - var(--color-sens)));
|
|
|
|
/* border is masked to a cone, originating from the center towards the pointer */
|
|
mask-image:
|
|
conic-gradient(
|
|
from var(--pointer-°) at center, black 25%, transparent 40%, transparent 60%, black 75%
|
|
);
|
|
}
|
|
|
|
&::after {
|
|
|
|
/* mesh gradient background */
|
|
|
|
border: 1px solid transparent;
|
|
|
|
background:
|
|
radial-gradient(at 80% 55%, hsla(268,100%,76%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 69% 34%, hsla(349,100%,74%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 8% 6%, hsla(136,100%,78%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 41% 38%, hsla(192,100%,64%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 86% 85%, hsla(186,100%,74%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 82% 18%, hsla(52,100%,65%,1) 0px, transparent 50%) padding-box,
|
|
radial-gradient(at 51% 4%, hsla(12,100%,72%,1) 0px, transparent 50%) padding-box,
|
|
linear-gradient(#c299ff 0 100%) padding-box;
|
|
|
|
/* 5 radial masks to create a squircle-cut-out, and then a cone-gradient
|
|
originating from the center towards the pointer to highlight the edges */
|
|
mask-image:
|
|
linear-gradient( to bottom, black, black ),
|
|
radial-gradient( ellipse at 50% 50%, black 40%, transparent 65% ),
|
|
radial-gradient( ellipse at 66% 66%, black 5%, transparent 40% ),
|
|
radial-gradient( ellipse at 33% 33%, black 5%, transparent 40% ),
|
|
radial-gradient( ellipse at 66% 33%, black 5%, transparent 40% ),
|
|
radial-gradient( ellipse at 33% 66%, black 5%, transparent 40% ),
|
|
conic-gradient( from var(--pointer-°) at center, transparent 5%, black 15%, black 85%, transparent 95% );
|
|
|
|
mask-composite: subtract,add,add,add,add,add;
|
|
|
|
/* opacity increases as pointer gets near edge */
|
|
opacity: calc((var(--pointer-d) - var(--color-sens)) / (100 - var(--color-sens)));
|
|
mix-blend-mode: var(--blend);
|
|
|
|
}
|
|
|
|
& > .glow {
|
|
|
|
/* glowing border edges */
|
|
|
|
--outset: var(--pads);
|
|
|
|
/* outer padding so the glow can overflow the element without being masked */
|
|
inset: calc(var(--outset) * -1);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
|
|
/* glow is masked to a cone, originating from the center towards the pointer */
|
|
mask-image:
|
|
conic-gradient(
|
|
from var(--pointer-°) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%
|
|
);
|
|
|
|
/* opacity increases as pointer gets near edge */
|
|
opacity: calc((var(--pointer-d) - var(--glow-sens)) / (100 - var(--glow-sens)));
|
|
mix-blend-mode: var(--glow-blend);
|
|
|
|
&::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: var(--outset);
|
|
border-radius: inherit;
|
|
box-shadow:
|
|
inset 0 0 0 1px hsl( var(--glow-color) / 100%),
|
|
|
|
inset 0 0 1px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 60%)),
|
|
inset 0 0 3px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 50%)),
|
|
inset 0 0 6px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 40%)),
|
|
inset 0 0 15px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 30%)),
|
|
inset 0 0 25px 2px hsl( var(--glow-color) / calc(var(--glow-boost) + 20%)),
|
|
inset 0 0 50px 2px hsl( var(--glow-color) / calc(var(--glow-boost) + 10%)),
|
|
|
|
0 0 1px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 60%)),
|
|
0 0 3px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 50%)),
|
|
0 0 6px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 40%)),
|
|
0 0 15px 0 hsl( var(--glow-color) / calc(var(--glow-boost) + 30%)),
|
|
0 0 25px 2px hsl( var(--glow-color) / calc(var(--glow-boost) + 20%)),
|
|
0 0 50px 2px hsl( var(--glow-color) / calc(var(--glow-boost) + 10%));
|
|
;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card {
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0.1) 0px 1px 2px,
|
|
rgba(0, 0, 0, 0.1) 0px 2px 4px,
|
|
rgba(0, 0, 0, 0.1) 0px 4px 8px,
|
|
rgba(0, 0, 0, 0.1) 0px 8px 16px,
|
|
rgba(0, 0, 0, 0.1) 0px 16px 32px,
|
|
rgba(0, 0, 0, 0.1) 0px 32px 64px;
|
|
}
|
|
|
|
.inner {
|
|
text-align: center;
|
|
h2 {
|
|
color: inherit;
|
|
font-weight: 500;
|
|
font-size: 1.25em;
|
|
margin-block: 0.5em;
|
|
}
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5em 1em;
|
|
|
|
}
|
|
svg {
|
|
height: 24px;
|
|
}
|
|
}
|
|
|
|
h2 {
|
|
}
|
|
|
|
.card .inner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
container-type: inline-size;
|
|
position: relative;
|
|
overflow: auto;
|
|
z-index: 1;
|
|
}
|
|
|
|
.card .content {
|
|
padding: 1em;
|
|
font-weight: 300;
|
|
text-align: left;
|
|
line-height: 1.4;
|
|
color: color-mix(var(--fg), transparent 60%);
|
|
overflow: auto;
|
|
scrollbar-width: none;
|
|
mask-image: linear-gradient( to top, transparent 5px, black 2em);
|
|
|
|
& em,
|
|
& strong {
|
|
color: color-mix(var(--fg), transparent 40%);
|
|
}
|
|
|
|
& p {
|
|
opacity: 0;
|
|
animation: fadeContent 1.5s ease-in-out 2s both;
|
|
&:nth-child(2) {
|
|
animation-delay: 2.25s;
|
|
}
|
|
&:nth-child(3) {
|
|
animation-delay: 2.5s;
|
|
}
|
|
&:nth-child(4) {
|
|
animation-delay: 2.75s;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@keyframes fadeContent {
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body, html {
|
|
height: 100svh;
|
|
overflow: auto;
|
|
background: hsl(var(--h), 18%, 12%);
|
|
}
|
|
|
|
main {
|
|
justify-items: center;
|
|
align-content: center;
|
|
min-height: 100%;
|
|
}
|
|
|
|
|
|
|
|
.sun {
|
|
opacity: 0.25;
|
|
&:hover {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
.moon {
|
|
opacty: 1;
|
|
}
|
|
|
|
.sun, .moon {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.light {
|
|
--link: hsl(var(--canvas), 90%, 50%);
|
|
--linkh: hsl(150, 85%, 40%);
|
|
& .sun {
|
|
opacity: 1;
|
|
}
|
|
& .moon {
|
|
opacity: 0.25;
|
|
&:hover {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
h2 {
|
|
text-shadow: 0 1px 1px lightslategray;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
const $card = document.querySelector(".card");
|
|
|
|
const cardUpdate = (e) => {
|
|
|
|
const position = pointerPositionRelativeToElement( $card, e );
|
|
const [px,py] = position.pixels;
|
|
const [perx, pery] = position.percent;
|
|
const [dx,dy] = distanceFromCenter( $card, px, py );
|
|
const edge = closenessToEdge( $card, px, py );
|
|
const angle = angleFromPointerEvent( $card, dx, dy );
|
|
|
|
$card.style.setProperty('--pointer-x', `${round(perx)}%`);
|
|
$card.style.setProperty('--pointer-y', `${round(pery)}%`);
|
|
$card.style.setProperty('--pointer-°', `${round(angle)}deg`);
|
|
$card.style.setProperty('--pointer-d', `${round(edge * 100)}`);
|
|
|
|
$card.classList.remove('animating');
|
|
|
|
};
|
|
|
|
$card.addEventListener("pointermove", cardUpdate);
|
|
|
|
|
|
const centerOfElement = ($el) => {
|
|
const { left, top, width, height } = $el.getBoundingClientRect();
|
|
return [ width/2, height/2 ];
|
|
}
|
|
|
|
const pointerPositionRelativeToElement = ($el, e) => {
|
|
const pos = [e.clientX, e.clientY];
|
|
const { left, top, width, height } = $el.getBoundingClientRect();
|
|
const x = pos[0] - left;
|
|
const y = pos[1] - top;
|
|
const px = clamp((100 / width) * x);
|
|
const py = clamp((100 / height) * y);
|
|
return { pixels: [x,y], percent: [px,py] }
|
|
}
|
|
|
|
const angleFromPointerEvent = ($el, dx, dy ) => {
|
|
// in degrees
|
|
let angleRadians = 0;
|
|
let angleDegrees = 0;
|
|
if ( dx !== 0 || dy !== 0 ) {
|
|
angleRadians = Math.atan2(dy, dx);
|
|
angleDegrees = angleRadians * (180 / Math.PI) + 90;
|
|
if (angleDegrees < 0) {
|
|
angleDegrees += 360;
|
|
}
|
|
}
|
|
return angleDegrees;
|
|
}
|
|
|
|
const distanceFromCenter = ( $card, x, y ) => {
|
|
// in pixels
|
|
const [cx,cy] = centerOfElement( $card );
|
|
return [ x - cx, y - cy ];
|
|
}
|
|
|
|
const closenessToEdge = ( $card, x, y ) => {
|
|
// in fraction (0,1)
|
|
const [cx,cy] = centerOfElement( $card );
|
|
const [dx,dy] = distanceFromCenter( $card, x, y );
|
|
let k_x = Infinity;
|
|
let k_y = Infinity;
|
|
if (dx !== 0) {
|
|
k_x = cx / Math.abs(dx);
|
|
}
|
|
if (dy !== 0) {
|
|
k_y = cy / Math.abs(dy);
|
|
}
|
|
return clamp((1 / Math.min(k_x, k_y)), 0, 1);
|
|
}
|
|
|
|
const round = (value, precision = 3) => parseFloat(value.toFixed(precision));
|
|
|
|
const clamp = (value, min = 0, max = 100) =>
|
|
Math.min(Math.max(value, min), max);
|
|
|
|
/** code for the intro animation, not related to teh interaction */
|
|
|
|
const playAnimation = () => {
|
|
|
|
const angleStart = 110;
|
|
const angleEnd = 465;
|
|
|
|
$card.style.setProperty('--pointer-°', `${angleStart}deg`);
|
|
$card.classList.add('animating');
|
|
|
|
animateNumber({
|
|
ease: easeOutCubic,
|
|
duration: 500,
|
|
onUpdate: (v) => {
|
|
$card.style.setProperty('--pointer-d', v);
|
|
}
|
|
});
|
|
|
|
animateNumber({
|
|
ease: easeInCubic,
|
|
delay: 0,
|
|
duration: 1500,
|
|
endValue: 50,
|
|
onUpdate: (v) => {
|
|
const d = (angleEnd - angleStart) * (v / 100) + angleStart;
|
|
$card.style.setProperty('--pointer-°', `${d}deg`);
|
|
}
|
|
});
|
|
|
|
animateNumber({
|
|
ease: easeOutCubic,
|
|
delay: 1500,
|
|
duration: 2250,
|
|
startValue: 50,
|
|
endValue: 100,
|
|
onUpdate: (v) => {
|
|
const d = (angleEnd - angleStart) * (v / 100) + angleStart;
|
|
$card.style.setProperty('--pointer-°', `${d}deg`);
|
|
}
|
|
});
|
|
|
|
animateNumber({
|
|
ease: easeInCubic,
|
|
duration: 1500,
|
|
delay: 2500,
|
|
startValue: 100,
|
|
endValue: 0,
|
|
onUpdate: (v) => {
|
|
$card.style.setProperty('--pointer-d', v);
|
|
},
|
|
onEnd: () => {
|
|
$card.classList.remove('animating');
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
playAnimation();
|
|
|
|
|
|
function easeOutCubic(x) {
|
|
return 1 - Math.pow(1 - x, 3);
|
|
}
|
|
function easeInCubic(x) {
|
|
return x * x * x;
|
|
}
|
|
|
|
function animateNumber(options) {
|
|
const {
|
|
startValue = 0,
|
|
endValue = 100,
|
|
duration = 1000,
|
|
delay = 0,
|
|
onUpdate = () => {},
|
|
ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
onStart = () => {},
|
|
onEnd = () => {},
|
|
} = options;
|
|
|
|
const startTime = performance.now() + delay;
|
|
|
|
function update() {
|
|
const currentTime = performance.now();
|
|
const elapsed = currentTime - startTime;
|
|
const t = Math.min(elapsed / duration, 1); // Normalize to [0, 1]
|
|
const easedValue = startValue + (endValue - startValue) * ease(t); // Apply easing
|
|
|
|
onUpdate(easedValue);
|
|
|
|
if (t < 1) {
|
|
requestAnimationFrame(update); // Continue the animation
|
|
} else if (t >= 1) {
|
|
onEnd();
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
onStart();
|
|
requestAnimationFrame(update); // Start the animation after the delay
|
|
}, delay);
|
|
}
|
|
</script> |