UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

654 lines (653 loc) 23.1 kB
/** * Configuration object for the Analog Clock. * @typedef {Object} ClockConfig * @property {string} bgColor - The background color of the clock face (e.g., '#ffffff', 'rgb(0,0,0)'). * @property {string} borderColor - The color of the clock's outer border. * @property {number} borderWidth - The thickness of the outer border in pixels. * @property {string} markColor - The color of the minute/hour tick marks. * @property {string} hourHandColor - The color of the hour hand. * @property {string} minuteHandColor - The color of the minute hand. * @property {string} secondHandColor - The color of the second hand. * @property {string} textColor - The color of the numbers on the clock face. * @property {string|null} skinUrl - The URL of an image to use as the clock face background. Set to null to use bgColor. * @property {boolean} showNumbers - Whether to render the numbers (1-12) on the clock face. * @property {boolean} showSeconds - Whether to display the second hand. * @property {number} size - The total width and height of the clock in pixels. * @property {number} sizeAdjust - Scale factor for the font size of the numbers relative to the clock size. * @property {number} padding - Padding in pixels between the clock edge and the tick marks. * @property {number} angleDistance - Distance multiplier (0-1) for placing numbers relative to the radius. * @property {number} pwH - Hour tick width as a percentage of clock size (0.0 to 1.0). * @property {number} phH - Hour tick height as a percentage of clock size (0.0 to 1.0). * @property {number} pwM - Minute tick width as a percentage of clock size (0.0 to 1.0). * @property {number} phM - Minute tick height as a percentage of clock size (0.0 to 1.0). */ class TinyAnalogClock { /** @type {HTMLElement} */ #element; /** @type {HTMLElement} */ #faceLayer; /** @type {HTMLElement} */ #skinLayer; /** @type {ClockConfig} */ #config; /** @type {number|null} */ #animationFrame = null; /** * Retrieves the current date and extracts the specific hour, minute, and second components. * @returns {{ now: Date, s: number, m: number, h: number }} * @private */ _getDate() { const now = new Date(); const s = now.getSeconds(); const m = now.getMinutes(); const h = now.getHours(); return { now, s, m, h }; } /** * Creates an instance of TinyAnalogClock. * Initializes the DOM structure and starts the animation loop. * @param {Partial<ClockConfig>} [options] - Optional configuration overrides. */ constructor(options = {}) { this.#config = { bgColor: '#f0f0f0', borderColor: '#333', borderWidth: 8, markColor: '#333', hourHandColor: '#000', minuteHandColor: '#444', secondHandColor: '#d81c1c', textColor: '#000', skinUrl: null, showNumbers: true, showSeconds: true, size: 800, sizeAdjust: 0.04, padding: 45, angleDistance: 0.95, pwH: 0.008, phH: 0.08, pwM: 0.005, phM: 0.03, ...options, }; this.#element = document.createElement('div'); this.#element.className = 'analog-clock-container'; // Base styles for the container this.#element.style.position = 'relative'; this.#element.style.borderRadius = '50%'; this.#element.style.overflow = 'hidden'; this.#element.style.boxSizing = 'border-box'; // 1. Skin Layer (Background Image) this.#skinLayer = document.createElement('div'); this.#skinLayer.style.position = 'absolute'; this.#skinLayer.style.inset = '0'; this.#skinLayer.style.backgroundSize = 'cover'; this.#skinLayer.style.backgroundPosition = 'center'; this.#skinLayer.style.zIndex = '0'; // 2. Face Layer (Ticks and Numbers) this.#faceLayer = document.createElement('div'); this.#faceLayer.style.position = 'absolute'; this.#faceLayer.style.inset = '0'; this.#faceLayer.style.zIndex = '1'; this.#faceLayer.style.pointerEvents = 'none'; // 3. Hands Layer (Hour, Minute, Second hands) const handsLayer = document.createElement('div'); handsLayer.style.position = 'absolute'; handsLayer.style.inset = '0'; handsLayer.style.zIndex = '2'; handsLayer.style.pointerEvents = 'none'; handsLayer.innerHTML = ` <div class="hand hour-hand"></div> <div class="hand minute-hand"></div> <div class="hand second-hand"></div> <div class="center-pin"></div> `; this.#element.appendChild(this.#skinLayer); this.#element.appendChild(this.#faceLayer); this.#element.appendChild(handsLayer); // Inject Styles dynamically to keep the class self-contained const style = document.createElement('style'); style.textContent = ` .analog-clock-container .hand { position: absolute; bottom: 50%; left: 50%; transform-origin: bottom center; border-radius: 4px; transform: translateX(-50%) rotate(0deg); z-index: 5; } .analog-clock-container .center-pin { position: absolute; top: 50%; left: 50%; width: 12px; height: 12px; background: #333; border-radius: 50%; transform: translate(-50%, -50%); z-index: 10; } .analog-clock-container .clock-mark { position: absolute; top: 50%; left: 50%; background: currentColor; transform-origin: center center; } .analog-clock-container .clock-number { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: sans-serif; font-weight: bold; text-align: center; line-height: 1; } `; this.#element.appendChild(style); this._applyConfig(); this._renderFace(); this._startTicker(); } /** * Applies the current configuration to the DOM elements (colors, sizes, visibility). * @private */ _applyConfig() { const s = this.#element.style; const c = this.#config; s.width = `${c.size}px`; s.height = `${c.size}px`; s.border = `${c.borderWidth}px solid ${c.borderColor}`; s.backgroundColor = c.bgColor; if (c.skinUrl) { this.#skinLayer.style.backgroundImage = `url(${c.skinUrl})`; this.#skinLayer.style.display = 'block'; } else { this.#skinLayer.style.display = 'none'; } /** * Helper to safely query elements within the clock. * @param {string} sel * @returns {HTMLDivElement} */ const q = (sel) => { const result = this.#element.querySelector(sel); if (!(result instanceof HTMLDivElement)) throw new Error(`TinyAnalogClock: Element ${sel} not found.`); return result; }; // Hour Hand const hHand = q('.hour-hand'); hHand.style.backgroundColor = c.hourHandColor; hHand.style.width = `${c.size * 0.025}px`; hHand.style.height = `${c.size * 0.25}px`; hHand.style.marginLeft = `${(c.size * 0.025) / -2}px`; // Center alignment fix hHand.style.bottom = '50%'; hHand.style.left = '50%'; hHand.style.transformOrigin = 'bottom center'; // Minute Hand const mHand = q('.minute-hand'); mHand.style.backgroundColor = c.minuteHandColor; mHand.style.width = `${c.size * 0.015}px`; mHand.style.height = `${c.size * 0.35}px`; mHand.style.marginLeft = `${(c.size * 0.015) / -2}px`; // Second Hand const sHand = q('.second-hand'); if (c.showSeconds) { sHand.style.display = 'block'; sHand.style.backgroundColor = c.secondHandColor; sHand.style.width = `${c.size * 0.005}px`; sHand.style.height = `${c.size * 0.4}px`; sHand.style.marginLeft = `${(c.size * 0.005) / -2}px`; } else { sHand.style.display = 'none'; } q('.center-pin').style.background = c.borderColor; } /** * Renders the clock face, including tick marks and numbers, based on the current size and config. * Uses trigonometry to position elements perfectly from the center. * @private */ _renderFace() { this.#faceLayer.innerHTML = ''; const radius = this.#config.size / 2 - this.#config.borderWidth; // 1. Render Ticks (Lines) for (let i = 0; i < 60; i++) { const isHour = i % 5 === 0; const el = document.createElement('div'); el.className = 'clock-mark'; const w = isHour ? this.#config.size * this.#config.pwH : this.#config.size * this.#config.pwM; const h = isHour ? this.#config.size * this.#config.phH : this.#config.size * this.#config.phM; const color = this.#config.markColor; el.style.width = `${w}px`; el.style.height = `${h}px`; el.style.color = color; // Distance from center to the TICK's center // We want the tick to be close to the edge. // Distance = radius - padding - (half of tick height) const distanceFromCenter = radius - this.#config.padding - h / 2; // Logic: Start at center (50% 50%) -> Rotate -> Push Outwards el.style.transform = `translate(-50%, -50%) rotate(${i * 6}deg) translate(0, -${distanceFromCenter}px)`; this.#faceLayer.appendChild(el); } // 2. Render Numbers if (this.#config.showNumbers) { for (let i = 1; i <= 12; i++) { const angle = i * 30 * (Math.PI / 180); // Adjust radius for text position (80% of total radius) const dist = radius * this.#config.angleDistance; const x = radius + Math.sin(angle) * dist; const y = radius - Math.cos(angle) * dist; const num = document.createElement('div'); num.className = 'clock-number'; num.innerText = i.toString(); num.style.left = `${x}px`; num.style.top = `${y}px`; num.style.color = this.#config.textColor; num.style.fontSize = `${this.#config.size * this.#config.sizeAdjust}px`; this.#faceLayer.appendChild(num); } } } /** * Starts the RequestAnimationFrame loop to update hand positions. * @private */ _startTicker() { if (this.#animationFrame) return; const update = () => { const { s, m, h } = this._getDate(); // Calculate degrees const sDeg = s * 6; const mDeg = m * 6 + s * 0.1; const hDeg = (h % 12) * 30 + m * 0.5; /** * @param {string} sel * @returns {HTMLDivElement} */ const q = (sel) => { const result = this.#element.querySelector(sel); if (!(result instanceof HTMLDivElement)) throw new Error(`${sel} not found.`); return result; }; if (this.#element.querySelector('.second-hand')) { q('.second-hand').style.transform = `rotate(${sDeg}deg)`; } q('.minute-hand').style.transform = `rotate(${mDeg}deg)`; q('.hour-hand').style.transform = `rotate(${hDeg}deg)`; this.#animationFrame = requestAnimationFrame(update); }; update(); } // ========================================== // GETTERS & SETTERS (Full Access) // ========================================== /** * Gets the main HTML element of the clock. * @returns {HTMLElement} The clock container. */ get element() { return this.#element; } /** * Sets the size of the clock. * @param {number} value - The new size in pixels. Must be a positive number. * @throws {Error} If value is not a positive number. */ set size(value) { if (typeof value !== 'number' || value <= 0 || isNaN(value)) { throw new Error(`TinyAnalogClock: 'size' must be a positive number. Received: ${value}`); } this.#config.size = value; this._applyConfig(); this._renderFace(); } /** * Gets the clock size in pixels. * @returns {number} */ get size() { return this.#config.size; } /** * Sets the background image (skin) of the clock. * @param {string|null} url - The URL string of the image, or null to remove the skin. * @throws {Error} If value is not a string or null. */ set skinUrl(url) { if (url !== null && typeof url !== 'string') { throw new Error(`TinyAnalogClock: 'skin' must be a URL string or null. Received type: ${typeof url}`); } this.#config.skinUrl = url; this._applyConfig(); } /** * Gets the skin URL. * @returns {string|null} */ get skinUrl() { return this.#config.skinUrl; } /** * Sets the primary border color. * @param {string} color - A valid CSS color string. * @throws {Error} If value is not a non-empty string. */ set borderColor(color) { if (typeof color !== 'string' || color.trim() === '') { throw new Error(`TinyAnalogClock: 'borderColor' must be a non-empty string. Received: ${color}`); } this.#config.borderColor = color; this._applyConfig(); this._renderFace(); } /** * Gets the border color. * @returns {string} */ get borderColor() { return this.#config.borderColor; } /** * Sets the primary marks color. * @param {string} color - A valid CSS color string. * @throws {Error} If value is not a non-empty string. */ set markColor(color) { if (typeof color !== 'string' || color.trim() === '') { throw new Error(`TinyAnalogClock: 'markColor' must be a non-empty string. Received: ${color}`); } this.#config.markColor = color; this._applyConfig(); this._renderFace(); } /** * Gets the tick marks color. * @returns {string} */ get markColor() { return this.#config.markColor; } /** * Sets the primary text color. * @param {string} color - A valid CSS color string. * @throws {Error} If value is not a non-empty string. */ set textColor(color) { if (typeof color !== 'string' || color.trim() === '') { throw new Error(`TinyAnalogClock: 'textColor' must be a non-empty string. Received: ${color}`); } this.#config.textColor = color; this._applyConfig(); this._renderFace(); } /** * Gets the text color. * @returns {string} */ get textColor() { return this.#config.textColor; } /** * Toggles the visibility of numbers on the clock face. * @param {boolean} value - True to show numbers, false to hide. * @throws {Error} If value is not a boolean. */ set showNumbers(value) { if (typeof value !== 'boolean') { throw new Error(`TinyAnalogClock: 'showNumbers' must be a boolean. Received type: ${typeof value}`); } this.#config.showNumbers = value; this._renderFace(); } /** * Gets numbers visibility. * @returns {boolean} */ get showNumbers() { return this.#config.showNumbers; } /** * Sets the background color. * @param {string} value */ set bgColor(value) { if (typeof value !== 'string' || !value) throw new Error("TinyAnalogClock: 'bgColor' must be a non-empty string."); this.#config.bgColor = value; this._applyConfig(); } /** * Gets the background color. * @returns {string} */ get bgColor() { return this.#config.bgColor; } /** * Sets the border thickness in pixels. * @param {number} value */ set borderWidth(value) { if (typeof value !== 'number' || value < 0) throw new Error("TinyAnalogClock: 'borderWidth' must be a non-negative number."); this.#config.borderWidth = value; this._applyConfig(); this._renderFace(); // Radius changes, so we must re-render face } /** * Gets the border thickness. * @returns {number} */ get borderWidth() { return this.#config.borderWidth; } /** * Sets the hour hand color. * @param {string} value */ set hourHandColor(value) { if (typeof value !== 'string' || !value) throw new Error("TinyAnalogClock: 'hourHandColor' must be a non-empty string."); this.#config.hourHandColor = value; this._applyConfig(); } /** * Gets the hour hand color. * @returns {string} */ get hourHandColor() { return this.#config.hourHandColor; } /** * Sets the minute hand color. * @param {string} value */ set minuteHandColor(value) { if (typeof value !== 'string' || !value) throw new Error("TinyAnalogClock: 'minuteHandColor' must be a non-empty string."); this.#config.minuteHandColor = value; this._applyConfig(); } /** * Gets the minute hand color. * @returns {string} */ get minuteHandColor() { return this.#config.minuteHandColor; } /** * Sets the second hand color. * @param {string} value */ set secondHandColor(value) { if (typeof value !== 'string' || !value) throw new Error("TinyAnalogClock: 'secondHandColor' must be a non-empty string."); this.#config.secondHandColor = value; this._applyConfig(); } /** * Gets the second hand color. * @returns {string} */ get secondHandColor() { return this.#config.secondHandColor; } /** * Toggle second hand visibility. * @param {boolean} value */ set showSeconds(value) { if (typeof value !== 'boolean') throw new Error("TinyAnalogClock: 'showSeconds' must be a boolean."); this.#config.showSeconds = value; this._applyConfig(); } /** * Gets second hand visibility. * @returns {boolean} */ get showSeconds() { return this.#config.showSeconds; } /** * Sets the font size adjustment factor. * @param {number} value - Positive number. */ set sizeAdjust(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'sizeAdjust' must be a positive number."); this.#config.sizeAdjust = value; this._renderFace(); } /** * Gets the font size adjustment factor. * @returns {number} */ get sizeAdjust() { return this.#config.sizeAdjust; } /** * Sets the padding from edge to ticks. * @param {number} value - Non-negative number in pixels. */ set padding(value) { if (typeof value !== 'number' || value < 0) throw new Error("TinyAnalogClock: 'padding' must be a non-negative number."); this.#config.padding = value; this._renderFace(); } /** * Gets the padding. * @returns {number} */ get padding() { return this.#config.padding; } /** * Sets the angle distance multiplier for numbers placement. * @param {number} value - Positive number. */ set angleDistance(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'angleDistance' must be a positive number."); this.#config.angleDistance = value; this._renderFace(); } /** * Gets the angle distance multiplier. * @returns {number} */ get angleDistance() { return this.#config.angleDistance; } /** * Sets the Hour tick width percentage. * @param {number} value - Positive number. */ set pwH(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'pwH' must be a positive number."); this.#config.pwH = value; this._renderFace(); } /** * Gets the Hour tick width percentage. * @returns {number} */ get pwH() { return this.#config.pwH; } /** * Sets the Hour tick height percentage. * @param {number} value - Positive number. */ set phH(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'phH' must be a positive number."); this.#config.phH = value; this._renderFace(); } /** * Gets the Hour tick height percentage. * @returns {number} */ get phH() { return this.#config.phH; } /** * Sets the Minute tick width percentage. * @param {number} value - Positive number. */ set pwM(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'pwM' must be a positive number."); this.#config.pwM = value; this._renderFace(); } /** * Gets the Minute tick width percentage. * @returns {number} */ get pwM() { return this.#config.pwM; } /** * Sets the Minute tick height percentage. * @param {number} value - Positive number. */ set phM(value) { if (typeof value !== 'number' || value <= 0) throw new Error("TinyAnalogClock: 'phM' must be a positive number."); this.#config.phM = value; this._renderFace(); } /** * Gets the Minute tick height percentage. * @returns {number} */ get phM() { return this.#config.phM; } /** * Destroys the clock instance, stops animations, and removes the element from DOM. * @returns {void} */ destroy() { if (this.#animationFrame) { cancelAnimationFrame(this.#animationFrame); this.#animationFrame = null; } this.#element.remove(); } } export default TinyAnalogClock;