Skip to content

Instantly share code, notes, and snippets.

@dackdel
Created November 6, 2025 12:16
Show Gist options
  • Select an option

  • Save dackdel/bbee124b689e758162d7c4938c786d57 to your computer and use it in GitHub Desktop.

Select an option

Save dackdel/bbee124b689e758162d7c4938c786d57 to your computer and use it in GitHub Desktop.
[javascript] ❍ Braun Style Inspired Clock
<div class="inspiration">
Braun Inspired Glass Clock
</div>
<div class="keyboard-info">
Press <kbd>H</kbd> to toggle settings panel | Using Berlin timezone
</div>
<svg style="position: absolute; width: 100%; height: 100%; z-index: 0;" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dottedGrid" width="30" height="30" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="1" fill="rgba(0,0,0,0.15)" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#dottedGrid)" />
</svg>
<div class="glass-clock-container">
<div class="glass-effect-wrapper">
<!-- Outer shadow - explicitly set opacity here -->
<div class="glass-effect-shadow" style="opacity: var(--outer-shadow-opacity);"></div>
<div class="glass-clock-face">
<!-- Base glossy layer - covers the entire clock face -->
<div class="glass-glossy-overlay" id="glass-glossy-overlay"></div>
<div class="glass-edge-highlight"></div>
<div class="glass-edge-shadow"></div>
<div class="glass-dark-edge"></div>
<div class="glass-reflection"></div>
<div class="glass-reflection-overlay" id="glass-reflection-overlay"></div>
<div class="clock-hour-marks" id="clock-hour-marks"></div>
<div class="hour-hand clock-hand" id="hour-hand"></div>
<div class="minute-hand clock-hand" id="minute-hand"></div>
<!-- New second hand structure -->
<div class="second-hand-container" id="second-hand-container">
<div class="second-hand"></div>
<div class="second-hand-counterweight"></div>
</div>
<!-- New second hand shadow structure -->
<div class="second-hand-shadow" id="second-hand-shadow"></div>
<div class="clock-center-dot"></div>
<div class="clock-center-blur"></div>
<div class="clock-logo">
<img src="https://upload.wikimedia.org/wikipedia/commons/1/16/Braun_Logo.svg" alt="Braun Logo" width="80" height="30" style="opacity: 0.8;">
</div>
<div class="clock-date" id="clock-date"></div>
<div class="clock-timezone" id="clock-timezone">Berlin</div>
</div>
</div>
</div>
<div class="attribution">
Inspired by <a href="https://codepen.io/Petr-Knoll/pen/QwWLZdx" target="_blank">Petr Knoll</a>
</div>
<div class="tweakpane-container" id="tweakpane-container"></div>
// Import Tweakpane as a module
import { Pane } from "https://cdn.jsdelivr.net/npm/tweakpane@4.0.5/dist/tweakpane.min.js";
// Wait for DOM to be fully loaded
document.addEventListener("DOMContentLoaded", function () {
// Create clock hour marks and numbers
const hourMarksContainer = document.getElementById("clock-hour-marks");
// Create hour numbers and minute markers with perfect spacing
for (let i = 0; i < 60; i++) {
if (i % 5 === 0) {
// Hour number (every 5 minutes)
const hourIndex = i / 5;
const hourNumber = document.createElement("div");
hourNumber.className = "clock-number";
// Calculate position for numbers - perfectly centered around the clock
const angle = (i * 6 * Math.PI) / 180;
const radius = 145; // Distance from center
const left = 175 + Math.sin(angle) * radius - 15;
const top = 175 - Math.cos(angle) * radius - 10;
hourNumber.style.left = `${left}px`;
hourNumber.style.top = `${top}px`;
hourNumber.textContent = hourIndex === 0 ? "12" : hourIndex.toString();
hourMarksContainer.appendChild(hourNumber);
} else {
// Minute marker (line)
const minuteMarker = document.createElement("div");
minuteMarker.className = "minute-marker";
minuteMarker.style.transform = `rotate(${i * 6}deg)`;
hourMarksContainer.appendChild(minuteMarker);
}
}
// Set fixed light angles
document.documentElement.style.setProperty("--primary-light-angle", "-45deg");
document.documentElement.style.setProperty("--dark-edge-angle", "135deg");
// Set fixed glossy overlay
const glossyOverlay = document.getElementById("glass-glossy-overlay");
if (glossyOverlay) {
glossyOverlay.style.background = `linear-gradient(135deg,
rgba(255, 255, 255, 0.9) 0%,
rgba(255, 255, 255, 0.7) 15%,
rgba(255, 255, 255, 0.5) 25%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0.2) 75%,
rgba(255, 255, 255, 0.1) 100%)`;
glossyOverlay.style.filter = "blur(10px)";
}
// Set fixed reflection overlay
const reflectionOverlay = document.getElementById("glass-reflection-overlay");
if (reflectionOverlay) {
reflectionOverlay.style.transform = "rotate(-15deg)";
reflectionOverlay.style.filter = "blur(10px)";
}
// Initialize Tweakpane
initTweakpane();
// Set the initial CSS variables
document.documentElement.style.setProperty("--inner-shadow-opacity", "0.15");
document.documentElement.style.setProperty("--reflection-opacity", "0.5");
document.documentElement.style.setProperty("--glossy-opacity", "0.3");
// Start the clock animation
startClock();
// Add keyboard shortcut to toggle Tweakpane visibility
document.addEventListener("keydown", function (event) {
if (event.key === "h" || event.key === "H") {
const tweakpaneContainer = document.getElementById("tweakpane-container");
if (tweakpaneContainer) {
tweakpaneContainer.style.display =
tweakpaneContainer.style.display === "none" ? "block" : "none";
}
}
});
});
// Variables for the seconds hand
let secondsAngle = 0;
let animationFrameId = null;
let secondsMode = "smooth"; // Default to smooth movement
// Function to start the clock
function startClock() {
// Get the current time for hour and minute hands
updateHourAndMinuteHands();
// Start the seconds hand animation based on the selected mode
animateSecondHand();
}
// Function to update hour and minute hands based on real time
function updateHourAndMinuteHands() {
const now = new Date();
const berlinTime = new Date(
now.toLocaleString("en-US", {
timeZone: "Europe/Berlin"
})
);
const hours = berlinTime.getHours() % 12;
const minutes = berlinTime.getMinutes();
const hourHand = document.getElementById("hour-hand");
const minuteHand = document.getElementById("minute-hand");
if (hourHand && minuteHand) {
const hoursDegrees = hours * 30 + (minutes / 60) * 30;
const minutesDegrees = minutes * 6;
hourHand.style.transform = `rotate(${hoursDegrees}deg)`;
minuteHand.style.transform = `rotate(${minutesDegrees}deg)`;
}
// Update date display with simple month and day format
const dateDisplay = document.getElementById("clock-date");
if (dateDisplay) {
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
const month = months[berlinTime.getMonth()];
const day = berlinTime.getDate();
dateDisplay.textContent = `${month} ${day}`;
}
// Update timezone display
const timezoneDisplay = document.getElementById("clock-timezone");
if (timezoneDisplay) {
timezoneDisplay.textContent = "Berlin";
}
// Update hour and minute hands every minute
setTimeout(updateHourAndMinuteHands, 60000);
}
// Function to animate the seconds hand based on the selected mode
function animateSecondHand() {
// Cancel any existing animation
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
const secondHandContainer = document.getElementById("second-hand-container");
const secondHandShadow = document.getElementById("second-hand-shadow");
if (!secondHandContainer) return;
// Different animation modes
switch (secondsMode) {
case "tick1": // Tick every second (60 ticks per minute)
animateTickMode(secondHandContainer, secondHandShadow, 6, 1); // 6 degrees per tick, 1 tick per second
break;
case "tick2": // Half-second ticks (120 ticks per minute)
animateTickMode(secondHandContainer, secondHandShadow, 3, 2); // 3 degrees per tick, 2 ticks per second
break;
case "highFreq": // High-frequency sweep (8 ticks per second)
animateTickMode(secondHandContainer, secondHandShadow, 0.75, 8); // 0.75 degrees per tick, 8 ticks per second
break;
case "smooth": // Smooth movement over 60 seconds
default:
animateSmoothMode(secondHandContainer, secondHandShadow);
break;
}
}
// Function to animate with ticking motion
function animateTickMode(
secondHandContainer,
secondHandShadow,
degreesPerTick,
ticksPerSecond
) {
let lastTickTime = 0;
const intervalMs = 1000 / ticksPerSecond;
function tick() {
// Get current time
const now = new Date();
const seconds = now.getSeconds();
const milliseconds = now.getMilliseconds();
// Calculate the current time in milliseconds within the minute
const timeInMs = seconds * 1000 + milliseconds;
// Calculate which tick we should be on
const tickIndex = Math.floor(timeInMs / intervalMs);
const currentTickTime = tickIndex * intervalMs;
// Only update if we've moved to a new tick
if (currentTickTime !== lastTickTime) {
lastTickTime = currentTickTime;
// Calculate the angle based on the tick index
// For high-frequency ticks, we need to ensure we complete a full rotation in 60 seconds
const totalTicksInRotation = ticksPerSecond * 60; // Total ticks in a full rotation
const currentTick = tickIndex % totalTicksInRotation;
secondsAngle = currentTick * (360 / totalTicksInRotation);
// Apply the rotation with a snap motion
secondHandContainer.style.transition = "none";
secondHandContainer.style.transform = `rotate(${secondsAngle}deg)`;
if (secondHandShadow) {
secondHandShadow.style.transition = "none";
secondHandShadow.style.transform = `rotate(${secondsAngle + 0.5}deg)`;
}
}
// Schedule the next check
setTimeout(tick, 10); // Check frequently to catch the exact tick moments
}
// Start ticking
tick();
}
// Function to animate with smooth motion
function animateSmoothMode(secondHandContainer, secondHandShadow) {
function animate() {
// Get current time with millisecond precision
const now = new Date();
const seconds = now.getSeconds();
const milliseconds = now.getMilliseconds();
// Calculate the exact angle based on real time
// Each second is 6 degrees (360/60), and we add the millisecond fraction
secondsAngle = seconds * 6 + (milliseconds / 1000) * 6;
// Apply the rotation smoothly
secondHandContainer.style.transition = "none"; // Changed to none for smoother movement
secondHandContainer.style.transform = `rotate(${secondsAngle}deg)`;
if (secondHandShadow) {
secondHandShadow.style.transition = "none"; // Changed to none for smoother movement
secondHandShadow.style.transform = `rotate(${secondsAngle + 0.5}deg)`;
}
// Request next frame for smooth animation
animationFrameId = requestAnimationFrame(animate);
}
// Start animation
animate();
}
// Initialize Tweakpane
function initTweakpane() {
try {
// Initialize Tweakpane with the imported module
const pane = new Pane({
container: document.getElementById("tweakpane-container"),
title: "Clock Settings"
});
// Create folders for organization
const visibilityFolder = pane.addFolder({
title: "Visibility"
});
const shadowsFolder = pane.addFolder({
title: "Shadows"
});
const colorsFolder = pane.addFolder({
title: "Colors"
});
const effectsFolder = pane.addFolder({
title: "Effects"
});
// Visibility controls
const params = {
minuteMarkerOpacity: 1,
innerShadowOpacity: 0.15,
reflectionOpacity: 1,
glossyOpacity: 0.8,
showNumbers: true,
hourNumberColor: "rgba(50, 50, 50, 0.9)",
minuteMarkerColor: "rgba(80, 80, 80, 0.5)",
handColor: "rgba(50, 50, 50, 0.9)",
secondHandColor: "rgba(255, 107, 0, 1)",
shadowLayer1: 0.1,
shadowLayer2: 0.1,
shadowLayer3: 0.1,
secondsMode: "smooth" // Default to smooth
};
// Visibility controls
visibilityFolder
.addBinding(params, "minuteMarkerOpacity", {
min: 0,
max: 1,
step: 0.1,
label: "Minute Markers"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--minute-marker-opacity",
ev.value
);
});
visibilityFolder
.addBinding(params, "reflectionOpacity", {
min: 0,
max: 1,
step: 0.1,
label: "Reflection"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--reflection-opacity",
ev.value
);
});
visibilityFolder
.addBinding(params, "glossyOpacity", {
min: 0,
max: 1,
step: 0.1,
label: "Glossy Effect"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--glossy-opacity",
ev.value
);
});
// Add toggle for numbers
visibilityFolder
.addBinding(params, "showNumbers", {
label: "Show Numbers"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--hour-number-opacity",
ev.value ? "1" : "0"
);
});
// Shadow controls - separated into inner and outer
shadowsFolder
.addBinding(params, "innerShadowOpacity", {
min: 0,
max: 1,
step: 0.1,
label: "Inner Shadow"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--inner-shadow-opacity",
ev.value
);
// Update inner shadow elements directly
const innerShadowElements = [
document.querySelector(".glass-edge-shadow"),
document.querySelector(".glass-dark-edge")
];
innerShadowElements.forEach((element) => {
if (element) {
element.style.opacity = ev.value;
}
});
});
// Add controls for the box-shadow layers in glass-clock-face
shadowsFolder
.addBinding(params, "shadowLayer1", {
min: 0,
max: 0.5,
step: 0.01,
label: "Shadow Layer 1"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--shadow-layer1-opacity",
ev.value
);
});
shadowsFolder
.addBinding(params, "shadowLayer2", {
min: 0,
max: 0.5,
step: 0.01,
label: "Shadow Layer 2"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--shadow-layer2-opacity",
ev.value
);
});
shadowsFolder
.addBinding(params, "shadowLayer3", {
min: 0,
max: 0.5,
step: 0.01,
label: "Shadow Layer 3"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--shadow-layer3-opacity",
ev.value
);
});
// Color controls
colorsFolder
.addBinding(params, "hourNumberColor", {
label: "Hour Numbers"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--hour-number-color",
ev.value
);
});
colorsFolder
.addBinding(params, "minuteMarkerColor", {
label: "Minute Markers"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--minute-marker-color",
ev.value
);
});
colorsFolder
.addBinding(params, "handColor", {
label: "Hour/Minute Hands"
})
.on("change", (ev) => {
document.documentElement.style.setProperty("--hand-color", ev.value);
});
colorsFolder
.addBinding(params, "secondHandColor", {
label: "Second Hand"
})
.on("change", (ev) => {
document.documentElement.style.setProperty(
"--second-hand-color",
ev.value
);
});
// Add seconds mode control with new options
effectsFolder
.addBinding(params, "secondsMode", {
options: {
"Smooth Movement": "smooth",
"Tick Every Second": "tick1",
"Half-Second Ticks": "tick2",
"High-Frequency Sweep": "highFreq"
},
label: "Seconds Mode"
})
.on("change", (ev) => {
secondsMode = ev.value;
animateSecondHand(); // Restart animation with new mode
});
// Effects controls
const effectsParams = {
lightAngle: -45,
darkEdgeAngle: 135
};
effectsFolder
.addBinding(effectsParams, "lightAngle", {
min: -180,
max: 180,
step: 1,
label: "Light Angle"
})
.on("change", (ev) => {
// Update the CSS variable
document.documentElement.style.setProperty(
"--primary-light-angle",
`${ev.value}deg`
);
});
effectsFolder
.addBinding(effectsParams, "darkEdgeAngle", {
min: -180,
max: 180,
step: 1,
label: "Dark Edge Angle"
})
.on("change", (ev) => {
// Update the CSS variable
document.documentElement.style.setProperty(
"--dark-edge-angle",
`${ev.value}deg`
);
});
// Add button to reset all settings
pane
.addButton({
title: "Reset All Settings"
})
.on("click", () => {
// Reset all parameters
params.minuteMarkerOpacity = 1;
params.innerShadowOpacity = 0.15;
params.reflectionOpacity = 0.5;
params.glossyOpacity = 0.3;
params.showNumbers = true;
params.hourNumberColor = "rgba(50, 50, 50, 0.9)";
params.minuteMarkerColor = "rgba(80, 80, 80, 0.5)";
params.handColor = "rgba(50, 50, 50, 0.9)";
params.secondHandColor = "rgba(255, 107, 0, 1)";
params.shadowLayer1 = 0.1;
params.shadowLayer2 = 0.1;
params.shadowLayer3 = 0.1;
params.secondsMode = "smooth";
effectsParams.lightAngle = -45;
effectsParams.darkEdgeAngle = 135;
// Apply all resets to CSS variables
document.documentElement.style.setProperty(
"--minute-marker-opacity",
"1"
);
document.documentElement.style.setProperty(
"--inner-shadow-opacity",
"0.15"
);
document.documentElement.style.setProperty(
"--reflection-opacity",
"0.5"
);
document.documentElement.style.setProperty("--glossy-opacity", "0.3");
document.documentElement.style.setProperty(
"--hour-number-opacity",
"1"
);
document.documentElement.style.setProperty(
"--hour-number-color",
"rgba(50, 50, 50, 0.9)"
);
document.documentElement.style.setProperty(
"--minute-marker-color",
"rgba(80, 80, 80, 0.5)"
);
document.documentElement.style.setProperty(
"--hand-color",
"rgba(50, 50, 50, 0.9)"
);
document.documentElement.style.setProperty(
"--second-hand-color",
"rgba(255, 107, 0, 1)"
);
document.documentElement.style.setProperty(
"--primary-light-angle",
"-45deg"
);
document.documentElement.style.setProperty(
"--dark-edge-angle",
"135deg"
);
document.documentElement.style.setProperty(
"--shadow-layer1-opacity",
"0.1"
);
document.documentElement.style.setProperty(
"--shadow-layer2-opacity",
"0.1"
);
document.documentElement.style.setProperty(
"--shadow-layer3-opacity",
"0.1"
);
// Update seconds mode
secondsMode = "smooth";
animateSecondHand();
// Update all shadow elements directly
const innerShadowElements = [
document.querySelector(".glass-edge-shadow"),
document.querySelector(".glass-dark-edge")
];
innerShadowElements.forEach((element) => {
if (element) {
element.style.opacity = 0.15;
}
});
const outerShadowElement = document.querySelector(
".glass-effect-shadow"
);
if (outerShadowElement) {
outerShadowElement.style.opacity = 1;
}
// Refresh the pane to show updated values
pane.refresh();
});
console.log("Tweakpane initialized successfully");
} catch (error) {
console.error("Error initializing Tweakpane:", error);
}
}
/* Custom CSS properties for light angle animations */
@property --primary-light-angle {
syntax: "<angle>";
inherits: false;
initial-value: -75deg;
}
@property --dark-edge-angle {
syntax: "<angle>";
inherits: false;
initial-value: 105deg;
}
:root {
--minute-marker-opacity: 1;
--inner-shadow-opacity: 1;
--outer-shadow-opacity: 1;
--reflection-opacity: 0.5;
--glossy-opacity: 0.3;
--hour-number-opacity: 1;
--hour-number-color: rgba(50, 50, 50, 0.9);
--minute-marker-color: rgba(80, 80, 80, 0.5);
--hand-color: rgba(50, 50, 50, 0.9);
--second-hand-color: rgba(255, 107, 0, 1);
--shadow-layer1-opacity: 0.1;
--shadow-layer2-opacity: 0.1;
--shadow-layer3-opacity: 0.1;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: rgba(215, 215, 215, 1);
font-family: "Inter", -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
.inspiration {
position: fixed;
top: 20px;
text-align: center;
font-size: 14px;
color: rgba(50, 50, 50, 0.8);
z-index: 100;
}
.inspiration a {
color: rgba(50, 50, 50, 0.9);
text-decoration: none;
transition: color 0.3s ease;
}
.inspiration a:hover {
color: rgba(0, 0, 0, 0.9);
text-decoration: underline;
}
.glass-clock-container {
position: relative;
width: 350px;
height: 350px;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
margin-top: 20px;
}
/* Glass effect wrapper */
.glass-effect-wrapper {
position: relative;
z-index: 2;
border-radius: 50%;
background: transparent;
pointer-events: none;
transition: all 400ms cubic-bezier(0.25, 1, 0.5, 1);
perspective: 1000px;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform;
}
/* Shadow container - OUTER SHADOW */
.glass-effect-shadow {
--shadow-offset: 3em;
position: absolute;
width: calc(100% + var(--shadow-offset));
height: calc(100% + var(--shadow-offset));
top: calc(0% - var(--shadow-offset) / 2);
left: calc(0% - var(--shadow-offset) / 2);
filter: blur(10px);
-webkit-filter: blur(10px);
pointer-events: none;
z-index: 1;
transition: all 400ms cubic-bezier(0.25, 1, 0.5, 1);
opacity: 1;
}
/* Shadow effect - softened with multiple layers */
.glass-effect-shadow::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.2));
width: calc(100% - var(--shadow-offset) - 0.25em);
height: calc(100% - var(--shadow-offset) - 0.25em);
top: calc(var(--shadow-offset) - 0.5em);
left: calc(var(--shadow-offset) - 0.875em);
padding: 0.125em;
box-sizing: border-box;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
overflow: visible;
opacity: 0.8;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05), 0 15px 25px rgba(0, 0, 0, 0.05),
0 20px 40px rgba(0, 0, 0, 0.05);
}
.glass-clock-face {
--border-width: clamp(2px, 0.0625em, 4px);
position: relative;
width: 350px;
height: 350px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.03);
background-image: linear-gradient(
-75deg,
rgba(255, 255, 255, 0.01),
rgba(255, 255, 255, 0.04),
rgba(255, 255, 255, 0.01)
);
/* Blur effect moved directly to the clock face */
backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
box-shadow: inset 0 0.4em 0.4em rgba(0, 0, 0, 0.1),
inset 0 -0.4em 0.4em rgba(255, 255, 255, 0.5),
10px 5px 10px rgba(0, 0, 0, var(--shadow-layer1-opacity)),
10px 20px 20px rgba(0, 0, 0, var(--shadow-layer2-opacity)),
10px 55px 50px rgba(0, 0, 0, var(--shadow-layer3-opacity));
z-index: 3;
overflow: hidden;
pointer-events: auto;
cursor: pointer;
transition: all 400ms cubic-bezier(0.25, 1, 0.5, 1);
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform, box-shadow;
}
/* Enhanced clock border effect with improved glossy edges based on the provided CSS */
.glass-clock-face::after {
content: "";
position: absolute;
z-index: 10;
inset: 0;
border-radius: 50%;
width: calc(100% + var(--border-width));
height: calc(100% + var(--border-width));
top: calc(0% - var(--border-width) / 2);
left: calc(0% - var(--border-width) / 2);
padding: var(--border-width);
box-sizing: border-box;
background: conic-gradient(
from var(--primary-light-angle) at 50% 50%,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0.2) 5% 40%,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0.2) 60% 95%,
rgba(255, 255, 255, 1)
),
linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.5));
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
box-shadow: inset 0 0 0 calc(var(--border-width) / 2) rgba(255, 255, 255, 0.9),
0 0 12px rgba(255, 255, 255, 0.8);
transition: all 400ms cubic-bezier(0.25, 1, 0.5, 1),
--primary-light-angle 500ms ease;
opacity: 0.9;
will-change: background, box-shadow;
}
/* Edge highlight ring */
.glass-edge-highlight {
position: absolute;
width: 350px;
height: 350px;
border-radius: 50%;
top: 0;
left: 0;
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
z-index: 8;
pointer-events: none;
opacity: 0.6;
}
/* Enhanced edge highlight ring with increased glossiness */
.glass-edge-highlight-outer {
position: absolute;
width: 356px;
height: 356px;
border-radius: 50%;
top: -3px;
left: -3px;
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
z-index: 7;
pointer-events: none;
opacity: 0.6;
}
/* Dark edge shadow for bottom-right contrast - INNER SHADOW */
.glass-edge-shadow {
position: absolute;
width: 350px;
height: 350px;
border-radius: 50%;
top: 0;
left: 0;
box-shadow: inset -5px 5px 15px rgba(0, 0, 0, 0.3),
inset -8px 8px 20px rgba(0, 0, 0, 0.2);
z-index: 7;
pointer-events: none;
opacity: var(--inner-shadow-opacity);
}
/* Dark edge effect for enhanced glossiness - INNER SHADOW */
.glass-dark-edge {
position: absolute;
z-index: 9;
inset: 0;
border-radius: 50%;
width: calc(100% + var(--border-width));
height: calc(100% + var(--border-width));
top: calc(0% - var(--border-width) / 2);
left: calc(0% - var(--border-width) / 2);
padding: var(--border-width);
box-sizing: border-box;
background: conic-gradient(
from var(--dark-edge-angle) at 50% 50%,
rgba(0, 0, 0, 0.5),
rgba(0, 0, 0, 0) 5% 40%,
rgba(0, 0, 0, 0.5) 50%,
rgba(0, 0, 0, 0) 60% 95%,
rgba(0, 0, 0, 0.5)
);
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
transition: all 400ms cubic-bezier(0.25, 1, 0.5, 1),
--dark-edge-angle 500ms ease;
box-shadow: inset 0 0 0 calc(var(--border-width) / 2) rgba(0, 0, 0, 0.2);
opacity: var(--inner-shadow-opacity);
will-change: background;
}
/* Prominent glossy overlay - covers the entire clock face */
.glass-glossy-overlay {
position: absolute;
width: 350px;
height: 350px;
border-radius: 50%;
top: 0;
left: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.9) 0%,
rgba(255, 255, 255, 0.7) 15%,
rgba(255, 255, 255, 0.5) 25%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0.2) 75%,
rgba(255, 255, 255, 0.1) 100%
);
pointer-events: none;
z-index: 6;
/* Below the clock elements but above the base */
mix-blend-mode: overlay;
opacity: var(--glossy-opacity);
filter: blur(10px);
}
/* Glass reflection overlay - new element */
.glass-reflection-overlay {
position: absolute;
width: 330px;
height: 330px;
border-radius: 50%;
top: 10px;
left: 10px;
background: radial-gradient(
ellipse at 30% 30%,
rgba(255, 255, 255, 0.6) 0%,
rgba(255, 255, 255, 0.3) 30%,
rgba(255, 255, 255, 0.1) 60%,
transparent 100%
);
pointer-events: none;
z-index: 15;
mix-blend-mode: overlay;
opacity: 0.7;
transform: rotate(-15deg);
filter: blur(10px);
}
/* Light reflection effect with enhanced glossiness */
.glass-reflection {
position: absolute;
width: 350px;
height: 175px;
top: 0;
left: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.7) 0%,
rgba(255, 255, 255, 0.4) 40%,
rgba(255, 255, 255, 0) 100%
);
border-radius: 175px 175px 0 0;
pointer-events: none;
z-index: 10;
mix-blend-mode: soft-light;
opacity: var(--reflection-opacity);
filter: blur(10px);
}
/* Hover effects - only for shadow */
.glass-effect-wrapper:hover .glass-effect-shadow {
/* Remove the dramatic shadow size change on hover */
opacity: 0.9;
}
.glass-effect-wrapper:hover .glass-effect-shadow::after {
opacity: 0.85;
}
/* Remove the hover effect for the glass reflection overlay */
.glass-effect-wrapper:hover .glass-reflection-overlay {
opacity: 0.8;
}
/* Modified to use scale3d for sharper scaling */
.glass-effect-wrapper:active {
transform: scale3d(0.97, 0.97, 1);
}
.glass-effect-wrapper:active .glass-clock-face {
box-shadow: inset 0 0.4em 0.4em rgba(0, 0, 0, 0.1),
inset 0 -0.4em 0.4em rgba(255, 255, 255, 0.4), 0 5px 20px rgba(0, 0, 0, 0.1),
0 10px 30px rgba(0, 0, 0, 0.1), 0 15px 40px rgba(0, 0, 0, 0.1);
}
.glass-effect-wrapper:active .glass-clock-face::after {
--primary-light-angle: -75deg;
}
.glass-effect-wrapper:active .glass-effect-shadow {
filter: blur(clamp(25px, 1.25em, 50px));
-webkit-filter: blur(clamp(25px, 1.25em, 50px));
width: calc(115% + var(--shadow-offset));
height: calc(115% + var(--shadow-offset));
}
.glass-effect-wrapper:active .glass-effect-shadow::after {
opacity: 0.9;
top: calc(var(--shadow-offset) - 0.7em);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.3));
}
/* Inner blur circle */
.clock-center-blur {
position: absolute;
width: 36px;
height: 36px;
top: 157px;
left: 157px;
background-color: rgba(255, 255, 255, 0.35);
border-radius: 50%;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 16;
pointer-events: none;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.4),
inset 0 0 8px rgba(255, 255, 255, 0.6);
}
.clock-hour-marks {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 14;
}
/* Minute marker as lines */
.minute-marker {
position: absolute;
width: 1px;
height: 10px;
background-color: var(--minute-marker-color);
top: 10px;
/* Moved closer to edge */
left: 175px;
transform-origin: center 165px;
/* Adjusted for edge position */
box-shadow: 0 0 2px rgba(255, 255, 255, 0.3);
opacity: var(--minute-marker-opacity);
}
.clock-number {
position: absolute;
font-size: 16px;
font-weight: 500;
color: var(--hour-number-color);
text-align: center;
width: 30px;
height: 20px;
line-height: 20px;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
z-index: 15;
opacity: var(--hour-number-opacity);
transition: opacity 0.3s ease;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
pointer-events: none;
}
.clock-logo {
position: absolute;
width: 100%;
height: 20px;
top: 110px;
left: 135px;
z-index: 15;
opacity: 0.9;
}
.clock-logo img {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
.clock-date {
position: absolute;
font-size: 12px;
font-weight: 400;
color: rgba(50, 50, 50, 0.8);
text-align: center;
width: 140px;
height: auto;
line-height: 1;
bottom: 115px;
left: 105px;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.3);
z-index: 15;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
pointer-events: none;
}
.clock-timezone {
position: absolute;
font-size: 12px;
font-weight: 400;
color: rgba(50, 50, 50, 0.8);
text-align: center;
width: 140px;
height: auto;
line-height: 1;
bottom: 100px;
left: 105px;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.3);
z-index: 15;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
pointer-events: none;
}
.clock-hand {
position: absolute;
transform-origin: center bottom;
bottom: 175px;
left: 175px;
z-index: 15;
will-change: transform;
}
.hour-hand {
width: 6px;
height: 70px;
background-color: var(--hand-color);
margin-left: -3px;
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.minute-hand {
width: 4px;
height: 100px;
background-color: var(--hand-color);
margin-left: -2px;
border-radius: 2px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
/* Clock center dot */
.clock-center-dot {
position: absolute;
width: 12px;
height: 12px;
background: var(--second-hand-color);
border-radius: 50%;
top: 169px;
left: 169px;
z-index: 17;
box-shadow: 0 0 8px rgba(255, 107, 0, 0.4);
}
/* Second hand container */
.second-hand-container {
position: absolute;
width: 2px;
height: 120px;
top: 55px;
/* Position it to extend from the center upward */
left: 174px;
/* Center it horizontally (175px - 1px for width/2) */
transform-origin: 1px 120px;
/* Set rotation origin to the bottom of the hand */
z-index: 17;
will-change: transform;
}
/* Second hand */
.second-hand {
position: absolute;
width: 2px;
height: 120px;
background-color: var(--second-hand-color);
bottom: 0;
left: 0;
box-shadow: 0 0 5px rgba(255, 107, 0, 0.5);
}
/* Second hand counterweight */
.second-hand-counterweight {
position: absolute;
width: 6px;
height: 14px;
background-color: var(--second-hand-color);
bottom: -14px;
/* Position it below the rotation point */
left: -2px;
border-radius: 0 0 4px 4px;
/* Rounded at the bottom */
box-shadow: 0 0 5px rgba(255, 107, 0, 0.5);
}
/* Second hand shadow */
.second-hand-shadow {
position: absolute;
width: 2px;
height: 120px;
top: 55px;
/* Match container position */
left: 174px;
/* Match container position */
transform-origin: 1px 120px;
/* Match container transform origin */
z-index: 14;
filter: blur(2px);
opacity: 0.3;
will-change: transform;
}
/* Second hand shadow line */
.second-hand-shadow::before {
content: "";
position: absolute;
width: 2px;
height: 120px;
background: transparent;
bottom: 0;
left: 0;
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.15);
}
/* Second hand shadow counterweight */
.second-hand-shadow::after {
content: "";
position: absolute;
width: 8px;
height: 16px;
background: transparent;
bottom: -16px;
/* Match counterweight position */
left: -3px;
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.15);
}
.attribution {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: rgba(50, 50, 50, 0.7);
text-align: center;
z-index: 100;
}
.attribution a {
color: rgba(50, 50, 50, 0.9);
text-decoration: none;
transition: color 0.3s ease;
}
.attribution a:hover {
color: rgba(0, 0, 0, 0.9);
}
.keyboard-info {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: rgba(50, 50, 50, 0.7);
text-align: center;
z-index: 100;
}
.keyboard-info kbd {
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
padding: 1px 4px;
font-family: monospace;
}
.tweakpane-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
padding: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
min-width: 250px;
display: none;
/* Hidden by default */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment