UNPKG

glassheart-ui-vanilla

Version:

GlassHeart UI - Vanilla JavaScript components

1,449 lines (1,438 loc) 52.3 kB
'use strict'; class GlassCard { constructor(options = {}) { this.options = { size: 'md', variant: 'default', glass: 'medium', liquid: false, interactive: false, loading: false, ...options }; this.element = this.createElement(); } createElement() { const card = document.createElement('div'); card.className = this.getClassNames(); if (this.options.content) { card.innerHTML = this.options.content; } return card; } getClassNames() { const classes = ['gh-card']; // Size classes if (this.options.size) { classes.push(`gh-card-${this.options.size}`); } // Variant classes if (this.options.variant) { classes.push(`gh-card-${this.options.variant}`); } // Glass effect if (this.options.glass) { classes.push(`gh-glass-${this.options.glass}`); } // Liquid effect if (this.options.liquid) { classes.push('gh-liquid-flow'); } // Interactive if (this.options.interactive) { classes.push('gh-interactive'); } // Loading state if (this.options.loading) { classes.push('gh-loading'); } return classes.join(' '); } render(container) { const target = typeof container === 'string' ? document.querySelector(container) : container; if (target) { target.appendChild(this.element); } } destroy() { if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } } getElement() { return this.element; } } /** * Creating the displacement map that is used by feDisplacementMap filter. * Uses dynamic dimensions based on element size. */ const getDisplacementMap = ({ width, height, radius, depth }) => { // 使用實際尺寸,確保精確對齊 const actualWidth = Math.max(width, 1); const actualHeight = Math.max(height, 1); // 計算邊框半徑的百分比 const radiusXPercent = (radius / actualWidth) * 100; const radiusYPercent = (radius / actualHeight) * 100; return "data:image/svg+xml;utf8," + encodeURIComponent(`<svg height="${actualHeight}" width="${actualWidth}" viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg"> <style> .mix { mix-blend-mode: screen; } </style> <defs> <linearGradient id="Y" x1="0" x2="0" y1="${Math.max(radiusYPercent * 0.5, 2)}%" y2="${Math.min(100 - radiusYPercent * 0.5, 98)}%"> <stop offset="0%" stop-color="#0F0" /> <stop offset="100%" stop-color="#000" /> </linearGradient> <linearGradient id="X" x1="${Math.max(radiusXPercent * 0.5, 2)}%" x2="${Math.min(100 - radiusXPercent * 0.5, 98)}%" y1="0" y2="0"> <stop offset="0%" stop-color="#F00" /> <stop offset="100%" stop-color="#000" /> </linearGradient> </defs> <rect x="0" y="0" height="${actualHeight}" width="${actualWidth}" fill="#808080" /> <g filter="blur(0.5px)"> <rect x="0" y="0" height="${actualHeight}" width="${actualWidth}" fill="#000080" /> <rect x="0" y="0" height="${actualHeight}" width="${actualWidth}" fill="url(#Y)" class="mix" /> <rect x="0" y="0" height="${actualHeight}" width="${actualWidth}" fill="url(#X)" class="mix" /> <rect x="${Math.max(depth, 1)}" y="${Math.max(depth, 1)}" height="${actualHeight - 2 * Math.max(depth, 1)}" width="${actualWidth - 2 * Math.max(depth, 1)}" fill="#808080" rx="${radius}" ry="${radius}" filter="blur(${Math.max(depth * 0.3, 0.5)}px)" /> </g> </svg>`); }; /** * SVG 快取工具,用於優化 Liquid Glass 效能的 SVG 生成 */ class SVGCache { constructor() { this.cache = new Map(); this.maxSize = 100; // 最大快取條目數 this.maxAge = 5 * 60 * 1000; // 5分鐘過期 } generateKey(key) { return `${key.width}x${key.height}_r${key.radius}_d${key.depth}_s${key.strength}_c${key.chromaticAberration}`; } isExpired(entry) { return Date.now() - entry.timestamp > this.maxAge; } cleanup() { const now = Date.now(); const entries = Array.from(this.cache.entries()); // 移除過期條目 entries.forEach(([key, entry]) => { if (now - entry.timestamp > this.maxAge) { this.cache.delete(key); } }); // 如果仍然超過最大大小,移除最舊的條目 if (this.cache.size > this.maxSize) { const sortedEntries = entries .filter(([key]) => this.cache.has(key)) .sort((a, b) => a[1].timestamp - b[1].timestamp); const toRemove = sortedEntries.slice(0, this.cache.size - this.maxSize); toRemove.forEach(([key]) => this.cache.delete(key)); } } get(key) { const cacheKey = this.generateKey(key); const entry = this.cache.get(cacheKey); if (!entry || this.isExpired(entry)) { if (entry) { this.cache.delete(cacheKey); } return null; } return entry.url; } set(key, url) { const cacheKey = this.generateKey(key); this.cache.set(cacheKey, { url, timestamp: Date.now(), }); // 定期清理快取 if (this.cache.size > this.maxSize * 0.8) { this.cleanup(); } } clear() { this.cache.clear(); } size() { return this.cache.size; } } // 導出單例實例 const svgCache = new SVGCache(); /** * Creating the displacement filter. * Uses dynamic dimensions based on element size. * The file complexity is due to the experimental "chromatic aberration" effect; * filters from first `feColorMatrix` to last `feBlend` can be removed if the effect is not needed. */ const getDisplacementFilter = ({ width, height, radius, depth, strength = 100, chromaticAberration = 0, }) => { // 使用實際尺寸,確保精確對齊 const actualWidth = Math.max(width, 1); const actualHeight = Math.max(height, 1); // 檢查快取 const cacheKey = { width: actualWidth, height: actualHeight, radius, depth, strength, chromaticAberration, }; const cached = svgCache.get(cacheKey); if (cached) { return cached; } // 生成新的 SVG const svgContent = `data:image/svg+xml;utf8,` + encodeURIComponent(`<svg height="${actualHeight}" width="${actualWidth}" viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg"> <defs> <filter id="displace" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%"> <feImage x="0" y="0" height="${actualHeight}" width="${actualWidth}" href="${getDisplacementMap({ width: actualWidth, height: actualHeight, radius, depth })}" result="displacementMap" /> <feDisplacementMap in="SourceGraphic" in2="displacementMap" scale="${strength + chromaticAberration * 2}" xChannelSelector="R" yChannelSelector="G" /> <feColorMatrix type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="displacedR" /> <feDisplacementMap in="SourceGraphic" in2="displacementMap" scale="${strength + chromaticAberration}" xChannelSelector="R" yChannelSelector="G" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="displacedG" /> <feDisplacementMap in="SourceGraphic" in2="displacementMap" scale="${strength}" xChannelSelector="R" yChannelSelector="G" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="displacedB" /> <feBlend in="displacedR" in2="displacedG" mode="screen"/> <feBlend in2="displacedB" mode="screen"/> </filter> </defs> </svg>`) + "#displace"; // 快取結果 svgCache.set(cacheKey, svgContent); return svgContent; }; const useLiquidGlass = (options = {}) => { const { depth: baseDepth = 8, strength = 100, chromaticAberration = 0, blur = 2, } = options; let elementRef = null; let state = { clicked: false, hovered: false, depth: baseDepth, }; let dimensions = { width: 0, height: 0, radius: 0 }; let debounceTimeout = null; let lastStyle = null; // 優化事件處理器,減少不必要的狀態更新 const handleMouseDown = () => { if (!state.clicked) { state.clicked = true; updateStyle(); } }; const handleMouseUp = () => { if (state.clicked) { state.clicked = false; updateStyle(); } }; // 使用防抖優化 hover 事件 const handleMouseEnter = () => { if (debounceTimeout) { clearTimeout(debounceTimeout); } debounceTimeout = window.setTimeout(() => { if (!state.hovered) { state.hovered = true; } }, 16); // 約 60fps }; const handleMouseLeave = () => { if (debounceTimeout) { clearTimeout(debounceTimeout); debounceTimeout = null; } if (state.hovered || state.clicked) { state.hovered = false; state.clicked = false; updateStyle(); } }; const updateDimensions = () => { if (elementRef) { const rect = elementRef.getBoundingClientRect(); const computedStyle = window.getComputedStyle(elementRef); const borderRadius = parseFloat(computedStyle.borderRadius) || 0; // 確保獲取精確的像素值 const width = Math.round(rect.width); const height = Math.round(rect.height); dimensions = { width, height, radius: borderRadius, }; updateStyle(); } }; // 使用 requestAnimationFrame 優化尺寸更新 const updateWithRAF = () => { requestAnimationFrame(updateDimensions); }; const updateStyle = () => { if (elementRef) { const currentDepth = state.clicked ? baseDepth / 0.7 : baseDepth; if (dimensions.width === 0 || dimensions.height === 0) { const backdropFilter = `blur(${blur / 2}px) brightness(1.1) saturate(1.5)`; if (lastStyle !== backdropFilter) { elementRef.style.backdropFilter = backdropFilter; lastStyle = backdropFilter; } return; } const displacementFilterUrl = getDisplacementFilter({ width: dimensions.width, height: dimensions.height, radius: dimensions.radius, depth: currentDepth, strength, chromaticAberration, }); const backdropFilter = `blur(${blur / 2}px) url('${displacementFilterUrl}') blur(${blur}px) brightness(1.1) saturate(1.5)`; // 只在樣式真正改變時才更新 if (lastStyle !== backdropFilter) { elementRef.style.backdropFilter = backdropFilter; lastStyle = backdropFilter; } } }; const getLiquidGlassStyle = () => { const currentDepth = state.clicked ? baseDepth / 0.7 : baseDepth; const displacementFilterUrl = getDisplacementFilter({ width: dimensions.width, height: dimensions.height, radius: dimensions.radius, depth: currentDepth, strength, chromaticAberration, }); const backdropFilter = `blur(${blur / 2}px) url('${displacementFilterUrl}') blur(${blur}px) brightness(1.1) saturate(1.5)`; return { backdropFilter }; }; const setupEventListeners = (element) => { element.addEventListener('mousedown', handleMouseDown); element.addEventListener('mouseup', handleMouseUp); element.addEventListener('mouseenter', handleMouseEnter); element.addEventListener('mouseleave', handleMouseLeave); window.addEventListener('resize', updateWithRAF); }; const cleanup = () => { // 清理防抖定時器 if (debounceTimeout) { clearTimeout(debounceTimeout); debounceTimeout = null; } if (elementRef) { elementRef.removeEventListener('mousedown', handleMouseDown); elementRef.removeEventListener('mouseup', handleMouseUp); elementRef.removeEventListener('mouseenter', handleMouseEnter); elementRef.removeEventListener('mouseleave', handleMouseLeave); } window.removeEventListener('resize', updateWithRAF); }; // Set element ref and setup listeners const setElementRef = (element) => { elementRef = element; setupEventListeners(element); updateWithRAF(); }; return { get elementRef() { return elementRef; }, set elementRef(element) { if (element) { setElementRef(element); } }, state, dimensions, getLiquidGlassStyle, handleMouseDown, handleMouseUp, handleMouseEnter, handleMouseLeave, cleanup, }; }; class GlassButton { constructor(options = {}) { this.options = { variant: 'default', shape: 'default', size: 'md', glass: 'medium', liquidGlass: false, liquidGlassOptions: {}, loading: false, disabled: false, ...options }; this.element = this.createElement(); this.setupLiquidGlass(); } createElement() { const button = document.createElement('button'); button.className = this.getClassNames(); button.textContent = this.options.text || 'Button'; button.disabled = this.options.disabled || this.options.loading || false; if (this.options.loading) { const spinner = document.createElement('span'); spinner.className = 'gh-loading-spinner'; button.appendChild(spinner); } if (this.options.onClick) { button.addEventListener('click', this.options.onClick); } if (this.options.onFocus) { button.addEventListener('focus', this.options.onFocus); } if (this.options.onBlur) { button.addEventListener('blur', this.options.onBlur); } if (this.options.onMouseEnter) { button.addEventListener('mouseenter', this.options.onMouseEnter); } if (this.options.onMouseLeave) { button.addEventListener('mouseleave', this.options.onMouseLeave); } return button; } setupLiquidGlass() { if (this.options.liquidGlass) { this.liquidGlassHook = useLiquidGlass({ depth: 8, strength: 100, chromaticAberration: 0, blur: 2, ...this.options.liquidGlassOptions, }); this.liquidGlassHook.elementRef = this.element; } } getClassNames() { const classes = ['gh-btn']; // Variant classes classes.push(`gh-btn-${this.options.variant}`); // Shape classes if (this.options.shape !== 'default') { classes.push(`gh-btn-${this.options.shape}`); } // Size classes classes.push(`gh-btn-${this.options.size}`); // Glass classes classes.push(`gh-glass-${this.options.glass}`); // Liquid glass classes if (this.options.liquidGlass) { classes.push('gh-btn-liquid-glass'); } // Loading classes if (this.options.loading) { classes.push('gh-btn-loading'); } return classes.filter(Boolean).join(' '); } getElement() { return this.element; } updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; this.element.className = this.getClassNames(); this.element.disabled = this.options.disabled || this.options.loading || false; // Update text content if (newOptions.text !== undefined) { this.element.textContent = this.options.text || 'Button'; } // Update loading state if (newOptions.loading !== undefined) { const existingSpinner = this.element.querySelector('.gh-loading-spinner'); if (this.options.loading && !existingSpinner) { const spinner = document.createElement('span'); spinner.className = 'gh-loading-spinner'; this.element.appendChild(spinner); } else if (!this.options.loading && existingSpinner) { existingSpinner.remove(); } } // Update liquid glass if (newOptions.liquidGlass !== undefined) { if (this.options.liquidGlass && !this.liquidGlassHook) { this.setupLiquidGlass(); } else if (!this.options.liquidGlass && this.liquidGlassHook) { this.liquidGlassHook.cleanup(); this.liquidGlassHook = undefined; } } } destroy() { if (this.liquidGlassHook) { this.liquidGlassHook.cleanup(); } this.element.remove(); } } class GlassInput { constructor(options = {}) { this.options = { type: 'text', size: 'md', variant: 'default', glass: 'medium', liquid: false, error: false, disabled: false, ...options }; this.element = this.createElement(); } createElement() { const input = document.createElement('input'); input.className = this.getClassNames(); input.type = this.options.type || 'text'; input.placeholder = this.options.placeholder || ''; input.disabled = this.options.disabled || false; if (this.options.value) { input.value = this.options.value; } if (this.options.onChange) { input.addEventListener('input', this.options.onChange); } if (this.options.onFocus) { input.addEventListener('focus', this.options.onFocus); } if (this.options.onBlur) { input.addEventListener('blur', this.options.onBlur); } return input; } getClassNames() { const classes = ['gh-input']; // Size classes if (this.options.size) { classes.push(`gh-input-${this.options.size}`); } // Variant classes if (this.options.variant) { classes.push(`gh-input-${this.options.variant}`); } // Glass effect if (this.options.glass) { classes.push(`gh-glass-${this.options.glass}`); } // Liquid effect if (this.options.liquid) { classes.push('gh-liquid-flow'); } // Error state if (this.options.error) { classes.push('gh-input-error'); } // Disabled state if (this.options.disabled) { classes.push('gh-disabled'); } return classes.join(' '); } render(container) { const target = typeof container === 'string' ? document.querySelector(container) : container; if (target) { target.appendChild(this.element); } } destroy() { if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } } getElement() { return this.element; } getValue() { return this.element.value; } setValue(value) { this.element.value = value; } setDisabled(disabled) { this.element.disabled = disabled; } } class GlassTypography { constructor(container, options) { this.isLoaded = false; this.dimensions = { width: 0, height: 0 }; this.element = container; this.options = { variant: 'p', size: 'md', weight: 'normal', glass: 'medium', liquid: false, gradient: false, animated: false, className: '', style: {}, fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', letterSpacing: 0, lineHeight: 1.2, textAlign: 'left', textShadow: true, glow: false, glowColor: '#ffffff', glowIntensity: 0.8, blur: 20, opacity: 0.2, saturation: 180, brightness: 1.2, contrast: 1.1, ...options, children: options.children || '', }; this.init(); } init() { this.createElements(); this.setupEventListeners(); this.updateDimensions(); this.drawGlassText(); } createElements() { // 創建容器 this.element.className = this.getComponentClasses(); this.element.style.cssText = this.getContainerStyle(); // 創建 Canvas this.canvas = document.createElement('canvas'); this.canvas.className = 'gh-typography-canvas'; this.canvas.style.cssText = this.getCanvasStyle(); // 創建 Fallback this.fallback = document.createElement('div'); this.fallback.className = 'gh-typography-fallback'; this.fallback.style.cssText = this.getFallbackStyle(); this.fallback.textContent = this.options.children; // 添加元素到容器 this.element.appendChild(this.canvas); this.element.appendChild(this.fallback); } setupEventListeners() { // 監聽窗口大小變化 window.addEventListener('resize', () => this.updateDimensions()); // 使用 ResizeObserver 監聽容器大小變化 if (window.ResizeObserver) { this.resizeObserver = new ResizeObserver(() => this.updateDimensions()); this.resizeObserver.observe(this.element); } // 動畫控制 if (this.options.animated && this.options.liquid) { this.animate(); } } updateDimensions() { const rect = this.element.getBoundingClientRect(); this.dimensions = { width: rect.width, height: rect.height }; if (this.dimensions.width > 0 && this.dimensions.height > 0) { this.drawGlassText(); } } // 計算字體大小 getFontSize() { const sizeMap = { xs: 12, sm: 14, md: 16, lg: 18, xl: 20, '2xl': 24, '3xl': 30, '4xl': 36, '5xl': 48, '6xl': 60, }; return sizeMap[this.options.size] || 16; } // 計算字體重量 getFontWeight() { const weightMap = { light: 300, normal: 400, medium: 500, semibold: 600, bold: 700, extrabold: 800, black: 900, }; return weightMap[this.options.weight] || 400; } // 計算玻璃效果參數 getGlassParams() { const glassMap = { light: { opacity: 0.15, blur: 15, saturation: 150, brightness: 1.3, contrast: 1.2 }, medium: { opacity: 0.25, blur: 25, saturation: 200, brightness: 1.4, contrast: 1.3 }, heavy: { opacity: 0.35, blur: 35, saturation: 250, brightness: 1.5, contrast: 1.4 }, }; return glassMap[this.options.glass] || glassMap.medium; } // 測量文字尺寸 measureText(text, font) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return { width: 0, height: 0 }; ctx.font = font; const metrics = ctx.measureText(text); const width = metrics.width; const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || 0; const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || 0; const height = ascent + descent; const fontSize = this.getFontSize(); const extraPadding = fontSize * 0.3; return { width, height: height + extraPadding }; } // 繪製高級毛玻璃文字 drawGlassText() { if (!this.canvas || !this.element || !this.options.children) return; const ctx = this.canvas.getContext('2d'); if (!ctx) return; const fontSize = this.getFontSize(); const fontWeight = this.getFontWeight(); const font = `${fontWeight} ${fontSize}px ${this.options.fontFamily}`; const glassParams = this.getGlassParams(); // 測量文字 const textMetrics = this.measureText(this.options.children, font); const textWidth = textMetrics.width; const textHeight = textMetrics.height; // 設置畫布尺寸 const rect = this.element.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const width = rect.width; const minHeight = Math.max(rect.height, textHeight + 40); this.canvas.width = width * dpr; this.canvas.height = minHeight * dpr; this.canvas.style.width = `${width}px`; this.canvas.style.height = `${minHeight}px`; ctx.scale(dpr, dpr); // 計算文字位置 let x = 0; let y = minHeight / 2; switch (this.options.textAlign) { case 'center': x = width / 2 - textWidth / 2; break; case 'right': x = width - textWidth; break; case 'justify': x = 0; break; default: x = 0; } // 清除畫布 ctx.clearRect(0, 0, width, minHeight); // 創建多層毛玻璃效果 const layers = 4; for (let layer = 0; layer < layers; layer++) { glassParams.blur + (layer * 3); // 設置字體 ctx.font = font; ctx.textAlign = this.options.textAlign; ctx.textBaseline = 'middle'; ctx.letterSpacing = `${this.options.letterSpacing}px`; // 創建複雜的漸變效果 const gradientObj = ctx.createLinearGradient(x - textWidth * 0.2, y - textHeight * 0.6, x + textWidth * 1.2, y + textHeight * 0.6); // 根據玻璃強度調整漸變 const baseOpacity = glassParams.opacity * (1 - layer * 0.2); const gradientStops = [ { pos: 0, alpha: baseOpacity * 0.9 }, { pos: 0.2, alpha: baseOpacity * 0.7 }, { pos: 0.4, alpha: baseOpacity * 0.5 }, { pos: 0.6, alpha: baseOpacity * 0.6 }, { pos: 0.8, alpha: baseOpacity * 0.8 }, { pos: 1, alpha: baseOpacity * 0.9 } ]; gradientStops.forEach(stop => { gradientObj.addColorStop(stop.pos, `rgba(255, 255, 255, ${stop.alpha})`); }); ctx.fillStyle = gradientObj; // 繪製多層陰影(深度效果) if (layer === 0) { if (this.options.glow) { // 發光效果 ctx.shadowColor = this.options.glowColor; ctx.shadowBlur = this.options.glowIntensity * 40; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } else { // 外層深陰影 ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; ctx.shadowBlur = 12; ctx.shadowOffsetX = 4; ctx.shadowOffsetY = 4; } } else if (layer === 1) { // 中層陰影 ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 6; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; } else if (layer === 2) { // 內層陰影 ctx.shadowColor = 'rgba(0, 0, 0, 0.15)'; ctx.shadowBlur = 3; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; } else { ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } // 繪製文字 ctx.fillText(this.options.children, x, y); // 重置陰影 ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } // 應用高級毛玻璃效果 const imageData = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i] || 0; const g = data[i + 1] || 0; const b = data[i + 2] || 0; const a = data[i + 3] || 0; if (a > 0) { const intensity = a / 255; // 應用強烈的亮度調整 data[i] = Math.min(255, r * this.options.brightness * 1.4); data[i + 1] = Math.min(255, g * this.options.brightness * 1.4); data[i + 2] = Math.min(255, b * this.options.brightness * 1.4); // 應用對比度調整 data[i] = Math.min(255, (data[i] - 128) * this.options.contrast + 128); data[i + 1] = Math.min(255, (data[i + 1] - 128) * this.options.contrast + 128); data[i + 2] = Math.min(255, (data[i + 2] - 128) * this.options.contrast + 128); // 應用飽和度調整 const gray = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = Math.min(255, gray + (data[i] - gray) * (this.options.saturation / 100)); data[i + 1] = Math.min(255, gray + (data[i + 1] - gray) * (this.options.saturation / 100)); data[i + 2] = Math.min(255, gray + (data[i + 2] - gray) * (this.options.saturation / 100)); // 創建玻璃透明度效果 const glassOpacity = Math.min(255, a * (0.6 + intensity * 0.4)); data[i + 3] = glassOpacity; } } ctx.putImageData(imageData, 0, 0); // 應用最終模糊效果 ctx.filter = `blur(${glassParams.blur * 0.3}px) saturate(${glassParams.saturation}%) brightness(${glassParams.brightness}) contrast(${glassParams.contrast})`; ctx.globalAlpha = 0.95; ctx.drawImage(this.canvas, 0, 0); // 重置濾鏡 ctx.filter = 'none'; ctx.globalAlpha = 1; this.isLoaded = true; this.updateElementClasses(); } // 動畫循環 animate() { if (this.options.animated && this.options.liquid) { this.drawGlassText(); this.animationRef = requestAnimationFrame(() => this.animate()); } } // 組件類名 getComponentClasses() { return [ 'gh-typography', `gh-typography-${this.options.variant}`, `gh-typography-${this.options.size}`, `gh-typography-${this.options.weight}`, `gh-glass-${this.options.glass}`, this.options.liquid ? 'gh-liquid-flow' : '', this.options.gradient ? 'gh-gradient' : '', this.options.animated ? 'gh-animated' : '', this.options.glow ? 'gh-glow' : '', this.isLoaded ? 'gh-loaded' : '', this.options.className, ].filter(Boolean).join(' '); } // 更新元素類名 updateElementClasses() { this.element.className = this.getComponentClasses(); } // 容器樣式 getContainerStyle() { const style = { ...this.options.style, fontFamily: this.options.fontFamily, letterSpacing: `${this.options.letterSpacing}px`, lineHeight: this.options.lineHeight, textAlign: this.options.textAlign, position: 'relative', display: 'inline-block', minHeight: Math.max(this.getFontSize() * this.options.lineHeight, this.getFontSize() + 30), padding: '15px 0', }; return Object.entries(style) .map(([key, value]) => `${key}: ${value}`) .join('; '); } // Canvas 樣式 getCanvasStyle() { const style = { position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '1', }; return Object.entries(style) .map(([key, value]) => `${key}: ${value}`) .join('; '); } // Fallback 樣式 getFallbackStyle() { const style = { opacity: this.isLoaded ? '0' : '1', position: 'relative', zIndex: '2', fontFamily: this.options.fontFamily, fontSize: `${this.getFontSize()}px`, fontWeight: this.getFontWeight(), letterSpacing: `${this.options.letterSpacing}px`, lineHeight: this.options.lineHeight, textAlign: this.options.textAlign, color: 'transparent', }; return Object.entries(style) .map(([key, value]) => `${key}: ${value}`) .join('; '); } // 更新選項 updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; this.element.className = this.getComponentClasses(); this.element.style.cssText = this.getContainerStyle(); this.fallback.style.cssText = this.getFallbackStyle(); this.fallback.textContent = this.options.children; if (this.options.animated && this.options.liquid) { this.animate(); } else if (this.animationRef) { cancelAnimationFrame(this.animationRef); } this.drawGlassText(); } // 銷毀 destroy() { if (this.animationRef) { cancelAnimationFrame(this.animationRef); } if (this.resizeObserver) { this.resizeObserver.disconnect(); } window.removeEventListener('resize', () => this.updateDimensions()); // 清理 DOM if (this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } if (this.fallback.parentNode) { this.fallback.parentNode.removeChild(this.fallback); } } } // 工廠函數 function createGlassTypography(container, options) { return new GlassTypography(container, options); } class GlassContainer { constructor(options = {}) { this.options = { size: 'md', variant: 'default', glass: 'medium', interactive: false, liquid: false, animated: false, padding: 'md', margin: 'none', rounded: 'md', shadow: 'md', overflow: 'visible', position: 'static', className: '', content: '', children: [], ...options, }; this.element = this.createElement(); this.setupEventListeners(); } createElement() { const element = document.createElement('div'); // Set classes const classes = this.getContainerClasses(); element.className = classes; // Set styles const styles = this.getContainerStyles(); Object.assign(element.style, styles); // Set content if (this.options.content) { element.innerHTML = this.options.content; } // Append children if (this.options.children && this.options.children.length > 0) { this.options.children.forEach(child => { element.appendChild(child); }); } return element; } getContainerClasses() { const baseClasses = 'gh-container'; const sizeClasses = `gh-container-${this.options.size}`; const variantClasses = this.options.variant !== 'default' ? `gh-container-${this.options.variant}` : ''; const glassClasses = `gh-glass-${this.options.glass}`; const interactiveClasses = this.options.interactive ? 'gh-container-interactive' : ''; const liquidClasses = this.options.liquid ? 'gh-container-liquid' : ''; const animatedClasses = this.options.animated ? 'gh-container-animated' : ''; const paddingClasses = this.options.padding !== 'none' ? `gh-p-${this.options.padding}` : ''; const marginClasses = this.options.margin !== 'none' ? `gh-m-${this.options.margin}` : ''; const roundedClasses = this.options.rounded !== 'none' ? `gh-rounded-${this.options.rounded}` : ''; const shadowClasses = this.options.shadow !== 'none' ? `gh-shadow-${this.options.shadow}` : ''; const overflowClasses = this.options.overflow !== 'visible' ? `gh-overflow-${this.options.overflow}` : ''; const positionClasses = this.options.position !== 'static' ? `gh-position-${this.options.position}` : ''; return [ baseClasses, sizeClasses, variantClasses, glassClasses, interactiveClasses, liquidClasses, animatedClasses, paddingClasses, marginClasses, roundedClasses, shadowClasses, overflowClasses, positionClasses, this.options.className, ] .filter(Boolean) .join(' '); } getContainerStyles() { const styles = {}; if (this.options.zIndex !== undefined) { styles.zIndex = this.options.zIndex.toString(); } return styles; } setupEventListeners() { if (this.options.onClick) { this.element.addEventListener('click', this.options.onClick); } if (this.options.onMouseEnter) { this.element.addEventListener('mouseenter', this.options.onMouseEnter); } if (this.options.onMouseLeave) { this.element.addEventListener('mouseleave', this.options.onMouseLeave); } } // Public methods render(selector) { const target = typeof selector === 'string' ? document.querySelector(selector) : selector; if (target) { target.appendChild(this.element); } else { console.error('Target element not found:', selector); } } appendTo(element) { element.appendChild(this.element); } remove() { if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } } update(options) { Object.assign(this.options, options); // Recreate element with new options const newElement = this.createElement(); if (this.element.parentNode) { this.element.parentNode.replaceChild(newElement, this.element); } this.element = newElement; this.setupEventListeners(); } getElement() { return this.element; } // Getters for current state get size() { return this.options.size || 'md'; } get variant() { return this.options.variant || 'default'; } get glass() { return this.options.glass || 'medium'; } get interactive() { return this.options.interactive || false; } get liquid() { return this.options.liquid || false; } get animated() { return this.options.animated || false; } // Static factory methods static create(options = {}) { return new GlassContainer(options); } static fromElement(element, options = {}) { const container = new GlassContainer(options); container.element = element; container.setupEventListeners(); return container; } } class GlassNavigation { constructor(options = {}) { this.isOpen = false; this.isScrolled = false; this.items = []; this.options = { variant: 'default', glass: 'medium', position: 'top', size: 'md', sticky: false, fixed: false, liquid: false, animated: false, blur: true, shadow: 'md', padding: 'md', rounded: 'full', className: '', ...options, }; this.element = this.createElement(); this.setupEventListeners(); } createElement() { const element = document.createElement('nav'); // Set classes const classes = this.getNavigationClasses(); element.className = classes; // Set styles const styles = this.getNavigationStyles(); Object.assign(element.style, styles); // Create container const container = document.createElement('div'); container.className = 'gh-navigation-container'; element.appendChild(container); return element; } getNavigationClasses() { const baseClasses = 'gh-navigation'; const variantClasses = `gh-navigation-${this.options.variant}`; const glassClasses = `gh-glass-${this.options.glass}`; const positionClasses = `gh-navigation-${this.options.position}`; const sizeClasses = `gh-navigation-${this.options.size}`; const stickyClasses = this.options.sticky ? 'gh-navigation-sticky' : ''; const fixedClasses = this.options.fixed ? 'gh-navigation-fixed' : ''; const liquidClasses = this.options.liquid ? 'gh-navigation-liquid' : ''; const animatedClasses = this.options.animated ? 'gh-navigation-animated' : ''; const blurClasses = this.options.blur ? 'gh-navigation-blur' : ''; const shadowClasses = this.options.shadow !== 'none' ? `gh-shadow-${this.options.shadow}` : ''; const paddingClasses = this.options.padding !== 'none' ? `gh-p-${this.options.padding}` : ''; const roundedClasses = this.options.rounded !== 'none' ? `gh-rounded-${this.options.rounded}` : ''; const scrolledClasses = this.isScrolled ? 'gh-navigation-scrolled' : ''; return [ baseClasses, variantClasses, glassClasses, positionClasses, sizeClasses, stickyClasses, fixedClasses, liquidClasses, animatedClasses, blurClasses, shadowClasses, paddingClasses, roundedClasses, scrolledClasses, this.options.className, ] .filter(Boolean) .join(' '); } getNavigationStyles() { const styles = {}; if (this.options.zIndex !== undefined) { styles.zIndex = this.options.zIndex.toString(); } return styles; } setupEventListeners() { if (this.options.sticky || this.options.fixed) { window.addEventListener('scroll', this.handleScroll.bind(this)); } } handleScroll() { this.isScrolled = window.scrollY > 10; this.updateClasses(); } updateClasses() { this.element.className = this.getNavigationClasses(); } // Public methods addBrand(text, href, onClick) { const brand = document.createElement(href ? 'a' : 'div'); brand.className = 'gh-navigation-brand'; brand.textContent = text; if (href) { brand.href = href; } if (onClick) { brand.addEventListener('click', onClick); } const container = this.element.querySelector('.gh-navigation-container'); if (container) { container.appendChild(brand); } } addItem(options) { const item = new GlassNavigationItem(options); this.items.push(item); const container = this.element.querySelector('.gh-navigation-container'); if (container) { // Create menu if it doesn't exist let menu = container.querySelector('.gh-navigation-menu'); if (!menu) { menu = document.createElement('div'); menu.className = 'gh-navigation-menu'; container.appendChild(menu); } menu.appendChild(item.getElement()); } return item; } addToggle() { const toggle = document.createElement('button'); toggle.className = 'gh-navigation-toggle'; toggle.setAttribute('aria-label', 'Toggle navigation menu'); // Add hamburger lines for (let i = 0; i < 3; i++) { const line = document.createElement('span'); line.className = 'gh-navigation-toggle-line'; toggle.appendChild(line); } toggle.addEventListener('click', () => { this.toggle(); }); const container = this.element.querySelector('.gh-navigation-container'); if (container) { container.appendChild(toggle); } } toggle() { this.isOpen = !this.isOpen; this.updateMenuState(); this.options.onToggle?.(this.isOpen); } updateMenuState() { const menu = this.element.querySelector('.gh-navigation-menu'); const toggle = this.element.querySelector('.gh-navigation-toggle'); if (menu) { if (this.isOpen) { menu.classList.add('gh-navigation-menu-open'); } else { menu.classList.remove('gh-navigation-menu-open'); } } if (toggle) { if (this.isOpen) { toggle.classList.add('gh-navigation-toggle-open'); } else { toggle.classList.remove('gh-navigation-toggle-open'); } } } render(selector) { const target = typeof selector === 'string' ? document.querySelector(selector) : selector; if (target) { target.appendChild(this.element); } else { console.error('Target element not found:', selector); } } appendTo(element) { element.appendChild(this.element); } remove() { if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } } update(options) { Object.assign(this.options, options); this.updateClasses(); } getElement() { return this.element; } // Static factory methods static create(options = {}) { return new GlassNavigation(options); } } class GlassNavigationItem { constructor(options) { this.options = { active: false, disabled: false, ...options, }; this.element = this.createElement(); this.setupEventListeners(); } createElement() { const isLink = !!this.options.href && !this.options.disabled; const element = isLink ? document.createElement('a') : document.createElement('button'); const baseClasses = 'gh-navigation-item'; const activeClasses = this.options.active ? 'gh-navigation-item-active' : ''; const disabledClasses = this.options.disabled ? 'gh-navigation-item-disabled' : ''; element.className = [baseClasses, activeClasses, disabledClasses] .filter(Boolean) .join(' '); if (isLink) { element.href = this.options.href; } if (this.options.disabled && element instanceof HTMLButtonElement) { element.disabled = true; } // Add icon if (this.options.icon) { const icon = document.createElement('span'); icon.className = 'gh-navigation-item-icon'; icon.textContent = this.options.icon; element.appendChild(icon); } // Add text const text = document.createElement('span'); text.className = 'gh-navigation-item-text'; text.textContent = this.options.text; element.appendChild(text); // Add badge if (this.options.badge) { const badge = document.createElement('span'); badge.className = 'gh-navigation-item-badge'; badge.textContent = this.options.badge.toString(); element.appendChild(badge); } return element; } setupEventListeners() { t