glassheart-ui-vanilla
Version:
GlassHeart UI - Vanilla JavaScript components
1,447 lines (1,437 loc) • 52.1 kB
JavaScript
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() {
this.element.add