htmlgen-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of htmlgen-mcp might be problematic. Click here for more details.
- MCP/__init__.py +6 -0
- MCP/web_agent_server.py +1257 -0
- agents/__init__.py +6 -0
- agents/smart_web_agent.py +2384 -0
- agents/web_tools/__init__.py +84 -0
- agents/web_tools/bootstrap.py +49 -0
- agents/web_tools/browser.py +28 -0
- agents/web_tools/colors.py +137 -0
- agents/web_tools/css.py +1473 -0
- agents/web_tools/edgeone_deploy.py +541 -0
- agents/web_tools/html_templates.py +1770 -0
- agents/web_tools/images.py +600 -0
- agents/web_tools/images_fixed.py +195 -0
- agents/web_tools/js.py +235 -0
- agents/web_tools/navigation.py +386 -0
- agents/web_tools/project.py +34 -0
- agents/web_tools/simple_builder.py +346 -0
- agents/web_tools/simple_css.py +475 -0
- agents/web_tools/simple_js.py +454 -0
- agents/web_tools/simple_templates.py +220 -0
- agents/web_tools/validation.py +65 -0
- htmlgen_mcp-0.2.0.dist-info/METADATA +171 -0
- htmlgen_mcp-0.2.0.dist-info/RECORD +26 -0
- htmlgen_mcp-0.2.0.dist-info/WHEEL +5 -0
- htmlgen_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- htmlgen_mcp-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""修复后的图片生成与注入工具"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def inject_images_fixed(
|
|
15
|
+
file_path: str,
|
|
16
|
+
provider: str = "pollinations",
|
|
17
|
+
topics: list | str | None = None,
|
|
18
|
+
size: str = "800x600",
|
|
19
|
+
seed: str | int | None = None,
|
|
20
|
+
save: bool = True,
|
|
21
|
+
subdir: str = "assets/images",
|
|
22
|
+
prefix: str = "bg",
|
|
23
|
+
) -> str:
|
|
24
|
+
"""修复版本的图片注入函数
|
|
25
|
+
|
|
26
|
+
专门修复原版本中的以下问题:
|
|
27
|
+
1. 占位符URL损坏
|
|
28
|
+
2. 重复的img标签
|
|
29
|
+
3. 图片下载失败
|
|
30
|
+
4. HTML结构混乱
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
|
|
36
|
+
# 解析尺寸
|
|
37
|
+
try:
|
|
38
|
+
w, h = [int(x) for x in str(size).lower().replace("*", "x").split("x")[:2]]
|
|
39
|
+
except Exception:
|
|
40
|
+
w, h = 800, 600
|
|
41
|
+
|
|
42
|
+
# 统一处理topics
|
|
43
|
+
if topics is None:
|
|
44
|
+
topics = []
|
|
45
|
+
elif isinstance(topics, str):
|
|
46
|
+
if topics.startswith("["):
|
|
47
|
+
try:
|
|
48
|
+
topics = json.loads(topics)
|
|
49
|
+
except Exception:
|
|
50
|
+
topics = [t.strip() for t in topics.split(",") if t.strip()]
|
|
51
|
+
else:
|
|
52
|
+
topics = [t.strip() for t in topics.split(",") if t.strip()]
|
|
53
|
+
|
|
54
|
+
# 如果没有提供topics,使用默认的运势相关主题
|
|
55
|
+
if not topics:
|
|
56
|
+
topics = [
|
|
57
|
+
"scorpio constellation night sky stars",
|
|
58
|
+
"mystic tarot cards purple background",
|
|
59
|
+
"golden astrology symbols",
|
|
60
|
+
"crystal ball fortune telling",
|
|
61
|
+
"zodiac wheel cosmic background"
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
def build_url(topic: str, idx: int) -> tuple[str, str | None]:
|
|
65
|
+
"""构建图片URL并可选下载"""
|
|
66
|
+
# 清理topic,避免包含HTML或特殊字符
|
|
67
|
+
clean_topic = re.sub(r'[<>"\']', '', str(topic)).strip()
|
|
68
|
+
clean_topic = clean_topic[:50] # 限制长度
|
|
69
|
+
|
|
70
|
+
if not clean_topic:
|
|
71
|
+
clean_topic = f"image-{idx + 1}"
|
|
72
|
+
|
|
73
|
+
topic_encoded = urllib.parse.quote_plus(clean_topic)
|
|
74
|
+
|
|
75
|
+
if provider.lower() in ("pollinations", "poll"):
|
|
76
|
+
url = f"https://image.pollinations.ai/prompt/{topic_encoded}?width={w}&height={h}&seed={seed or idx}"
|
|
77
|
+
ext = "jpg"
|
|
78
|
+
elif provider.lower() in ("dicebear", "avatar"):
|
|
79
|
+
url = f"https://api.dicebear.com/7.x/bottts/svg?seed={topic_encoded}"
|
|
80
|
+
ext = "svg"
|
|
81
|
+
elif provider.lower() in ("robohash", "robo"):
|
|
82
|
+
url = f"https://robohash.org/{topic_encoded}.png?size={w}x{h}"
|
|
83
|
+
ext = "png"
|
|
84
|
+
else:
|
|
85
|
+
# 默认使用pollinations
|
|
86
|
+
url = f"https://image.pollinations.ai/prompt/{topic_encoded}?width={w}&height={h}&seed={seed or idx}"
|
|
87
|
+
ext = "jpg"
|
|
88
|
+
|
|
89
|
+
saved_path = None
|
|
90
|
+
if save:
|
|
91
|
+
try:
|
|
92
|
+
root = Path(file_path).parent
|
|
93
|
+
out_dir = root / subdir
|
|
94
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
out_file = out_dir / f"{prefix}-{idx + 1}.{ext}"
|
|
96
|
+
|
|
97
|
+
# 下载图片
|
|
98
|
+
with urllib.request.urlopen(url, timeout=15) as resp:
|
|
99
|
+
data = resp.read()
|
|
100
|
+
with open(out_file, "wb") as fo:
|
|
101
|
+
fo.write(data)
|
|
102
|
+
saved_path = str(out_file)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"下载图片失败: {e}")
|
|
105
|
+
saved_path = None
|
|
106
|
+
|
|
107
|
+
return url, saved_path
|
|
108
|
+
|
|
109
|
+
def build_clean_placeholder(topic: str, idx: int) -> str:
|
|
110
|
+
"""构建干净的占位符URL"""
|
|
111
|
+
clean_topic = re.sub(r'[<>"\']', '', str(topic)).strip()
|
|
112
|
+
clean_topic = clean_topic[:20] if clean_topic else f"image-{idx + 1}"
|
|
113
|
+
encoded = urllib.parse.quote_plus(clean_topic)
|
|
114
|
+
return f"https://placehold.co/{w}x{h}/444/fff?text={encoded}"
|
|
115
|
+
|
|
116
|
+
# 清理现有的损坏图片标签
|
|
117
|
+
# 移除所有包含损坏URL的img标签
|
|
118
|
+
content = re.sub(r'<img[^>]*?placehold\.co/<re\.Match[^>]*?>', '', content, flags=re.IGNORECASE | re.DOTALL)
|
|
119
|
+
|
|
120
|
+
# 移除重复的空img标签
|
|
121
|
+
content = re.sub(r'<img[^>]*?data-topic="image-\d+"[^>]*?>\s*', '', content, flags=re.IGNORECASE)
|
|
122
|
+
|
|
123
|
+
# 寻找需要插入图片的位置
|
|
124
|
+
# 1. 首先处理hero背景图
|
|
125
|
+
hero_match = re.search(r'(<section[^>]*?class="[^"]*hero-section[^"]*"[^>]*>)', content, re.IGNORECASE)
|
|
126
|
+
if hero_match and topics:
|
|
127
|
+
url, saved = build_url(topics[0], 0)
|
|
128
|
+
target_url = saved or url
|
|
129
|
+
old_section = hero_match.group(1)
|
|
130
|
+
|
|
131
|
+
# 更新背景图片
|
|
132
|
+
bg_style = f"background-image: url('{target_url}'); background-size: cover; background-position: center; background-repeat: no-repeat;"
|
|
133
|
+
|
|
134
|
+
if 'style=' in old_section:
|
|
135
|
+
new_section = re.sub(r'style="([^"]*)"', lambda m: f'style="{m.group(1)}; {bg_style}"', old_section)
|
|
136
|
+
else:
|
|
137
|
+
new_section = old_section.replace('>', f' style="{bg_style}">')
|
|
138
|
+
|
|
139
|
+
content = content.replace(old_section, new_section)
|
|
140
|
+
|
|
141
|
+
# 2. 为每个卡片添加图片
|
|
142
|
+
card_pattern = r'(<div[^>]*?class="[^"]*card[^"]*"[^>]*>)\s*(<div[^>]*?class="[^"]*card-body[^"]*"[^>]*>)'
|
|
143
|
+
cards = list(re.finditer(card_pattern, content, re.IGNORECASE))
|
|
144
|
+
|
|
145
|
+
offset = 0
|
|
146
|
+
for i, match in enumerate(cards):
|
|
147
|
+
if i + 1 < len(topics):
|
|
148
|
+
topic = topics[i + 1]
|
|
149
|
+
else:
|
|
150
|
+
topic = f"mystical fortune symbol {i + 1}"
|
|
151
|
+
|
|
152
|
+
url, saved = build_url(topic, i + 1)
|
|
153
|
+
target_url = saved or url
|
|
154
|
+
placeholder = build_clean_placeholder(topic, i + 1)
|
|
155
|
+
|
|
156
|
+
# 创建图片标签
|
|
157
|
+
img_tag = f'''<img src="{target_url}" alt="{topic}" class="card-img-top" style="height: 200px; object-fit: cover;" loading="lazy" onerror="this.src='{placeholder}'">'''
|
|
158
|
+
|
|
159
|
+
# 插入到card-body之前
|
|
160
|
+
insert_pos = match.end(1) + offset
|
|
161
|
+
content = content[:insert_pos] + '\n ' + img_tag + '\n ' + content[insert_pos:]
|
|
162
|
+
offset += len(img_tag) + 10 # 考虑换行和缩进
|
|
163
|
+
|
|
164
|
+
# 保存修复后的文件
|
|
165
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
166
|
+
f.write(content)
|
|
167
|
+
|
|
168
|
+
return f"图片注入完成: 处理了 {len(cards) + 1} 个位置 (hero + {len(cards)} 张卡片图)"
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return f"图片注入失败: {str(e)}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
# 测试函数
|
|
176
|
+
test_file = "/Users/fengjinchao/Desktop/2/html/scorpio-daily-fortune/index.html"
|
|
177
|
+
result = inject_images_fixed(
|
|
178
|
+
test_file,
|
|
179
|
+
provider="pollinations",
|
|
180
|
+
topics=[
|
|
181
|
+
"scorpio constellation night sky stars mystical",
|
|
182
|
+
"love heart tarot cards romantic fortune",
|
|
183
|
+
"business career success symbols gold",
|
|
184
|
+
"money coins financial luck prosperity",
|
|
185
|
+
"lucky colors yellow golden sunshine",
|
|
186
|
+
"lucky numbers 2 mystical numerology",
|
|
187
|
+
"compass north direction feng shui",
|
|
188
|
+
"clock time 18:00 evening golden hour"
|
|
189
|
+
],
|
|
190
|
+
size="800x600",
|
|
191
|
+
save=True,
|
|
192
|
+
subdir="assets/images",
|
|
193
|
+
prefix="scorpio"
|
|
194
|
+
)
|
|
195
|
+
print(result)
|
agents/web_tools/js.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""JavaScript 脚本生成工具"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_js_file(file_path: str, content: str = ""):
|
|
8
|
+
"""创建JavaScript文件;自动创建父级目录"""
|
|
9
|
+
js_template = f"""// 页面加载完成后执行
|
|
10
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
|
11
|
+
// Smooth scroll 由 CSS scroll-behavior 支持,这里补充导航折叠与激活态处理
|
|
12
|
+
const navLinks = document.querySelectorAll('.navbar a.nav-link[href^="#"]');
|
|
13
|
+
navLinks.forEach(link => {{
|
|
14
|
+
link.addEventListener('click', () => {{
|
|
15
|
+
const navbarCollapse = document.querySelector('.navbar .navbar-collapse');
|
|
16
|
+
if (navbarCollapse && navbarCollapse.classList.contains('show')) {{
|
|
17
|
+
const toggler = document.querySelector('.navbar .navbar-toggler');
|
|
18
|
+
toggler && toggler.click();
|
|
19
|
+
}}
|
|
20
|
+
}});
|
|
21
|
+
}});
|
|
22
|
+
|
|
23
|
+
// 简易 ScrollSpy
|
|
24
|
+
const sections = Array.from(document.querySelectorAll('section[id]'));
|
|
25
|
+
const onScroll = () => {{
|
|
26
|
+
let activeId = null;
|
|
27
|
+
const scrollY = window.scrollY + 120; // 预留导航高度
|
|
28
|
+
sections.forEach(sec => {{
|
|
29
|
+
const rect = sec.getBoundingClientRect();
|
|
30
|
+
const top = window.scrollY + rect.top;
|
|
31
|
+
if (scrollY >= top) activeId = sec.id;
|
|
32
|
+
}});
|
|
33
|
+
if (activeId) {{
|
|
34
|
+
navLinks.forEach(a => a.classList.toggle('active', a.getAttribute('href') === '#' + activeId));
|
|
35
|
+
}}
|
|
36
|
+
|
|
37
|
+
// 玻璃态导航滚动态
|
|
38
|
+
const nav = document.querySelector('.navbar-glass');
|
|
39
|
+
if (nav) {{
|
|
40
|
+
nav.classList.toggle('scrolled', window.scrollY > 10);
|
|
41
|
+
}}
|
|
42
|
+
}};
|
|
43
|
+
window.addEventListener('scroll', onScroll, {{ passive: true }});
|
|
44
|
+
onScroll();
|
|
45
|
+
|
|
46
|
+
// Reveal 动画
|
|
47
|
+
const reveals = document.querySelectorAll('.reveal');
|
|
48
|
+
if ('IntersectionObserver' in window) {{
|
|
49
|
+
const io = new IntersectionObserver(entries => {{
|
|
50
|
+
entries.forEach(e => {{ if (e.isIntersecting) e.target.classList.add('revealed'); }});
|
|
51
|
+
}}, {{ threshold: 0.1 }});
|
|
52
|
+
reveals.forEach(el => io.observe(el));
|
|
53
|
+
}} else {{
|
|
54
|
+
reveals.forEach(el => el.classList.add('revealed'));
|
|
55
|
+
}}
|
|
56
|
+
|
|
57
|
+
// 主题切换(明/暗)
|
|
58
|
+
const applyTheme = (theme) => {{
|
|
59
|
+
const html = document.documentElement;
|
|
60
|
+
if (theme === 'dark') html.setAttribute('data-theme', 'dark');
|
|
61
|
+
else html.removeAttribute('data-theme');
|
|
62
|
+
}};
|
|
63
|
+
const savedTheme = localStorage.getItem('theme');
|
|
64
|
+
if (savedTheme) {{
|
|
65
|
+
applyTheme(savedTheme);
|
|
66
|
+
}} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {{
|
|
67
|
+
applyTheme('dark');
|
|
68
|
+
}}
|
|
69
|
+
const toggleBtn = document.querySelector('[data-action="toggle-theme"]');
|
|
70
|
+
toggleBtn && toggleBtn.addEventListener('click', () => {{
|
|
71
|
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
72
|
+
const next = isDark ? 'light' : 'dark';
|
|
73
|
+
localStorage.setItem('theme', next);
|
|
74
|
+
applyTheme(next);
|
|
75
|
+
}});
|
|
76
|
+
|
|
77
|
+
// 统计数字动画
|
|
78
|
+
const counters = document.querySelectorAll('.counter[data-target]');
|
|
79
|
+
const animateCounter = (el) => {{
|
|
80
|
+
const target = parseFloat(el.getAttribute('data-target')) || 0;
|
|
81
|
+
const duration = 1200;
|
|
82
|
+
const startTime = performance.now();
|
|
83
|
+
const startVal = 0;
|
|
84
|
+
const step = (now) => {{
|
|
85
|
+
const p = Math.min(1, (now - startTime) / duration);
|
|
86
|
+
const eased = p < 0.5 ? 2*p*p : -1 + (4 - 2*p)*p; // easeInOutQuad
|
|
87
|
+
const val = Math.floor(startVal + (target - startVal) * eased);
|
|
88
|
+
el.textContent = val.toString();
|
|
89
|
+
if (p < 1) requestAnimationFrame(step);
|
|
90
|
+
}};
|
|
91
|
+
requestAnimationFrame(step);
|
|
92
|
+
}};
|
|
93
|
+
if ('IntersectionObserver' in window) {{
|
|
94
|
+
const co = new IntersectionObserver(entries => {{
|
|
95
|
+
entries.forEach(e => {{ if (e.isIntersecting) {{ animateCounter(e.target); co.unobserve(e.target); }} }});
|
|
96
|
+
}}, {{ threshold: 0.4 }});
|
|
97
|
+
counters.forEach(el => co.observe(el));
|
|
98
|
+
}} else {{
|
|
99
|
+
counters.forEach(el => animateCounter(el));
|
|
100
|
+
}}
|
|
101
|
+
|
|
102
|
+
// AI 图片占位兜底(即使未执行 inject_images,也能避免破图)
|
|
103
|
+
const placeholderSVG = (text = 'Image') => {{
|
|
104
|
+
const svg = "<?xml version='1.0' encoding='UTF-8'?>" +
|
|
105
|
+
"<svg xmlns='http://www.w3.org/2000/svg' width='800' height='600'>" +
|
|
106
|
+
"<defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'>" +
|
|
107
|
+
"<stop offset='0%' stop-color='rgba(13,110,253,0.15)'/>" +
|
|
108
|
+
"<stop offset='100%' stop-color='rgba(102,16,242,0.15)'/></linearGradient></defs>" +
|
|
109
|
+
"<rect width='100%' height='100%' fill='url(#g)'/>" +
|
|
110
|
+
"<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='#99a2b3' font-size='24' font-family='-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial'>" +
|
|
111
|
+
text +
|
|
112
|
+
"</text></svg>";
|
|
113
|
+
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
114
|
+
}};
|
|
115
|
+
|
|
116
|
+
const phImg = placeholderSVG('Image');
|
|
117
|
+
document.querySelectorAll('img[data-topic]').forEach(img => {{
|
|
118
|
+
if (!img.getAttribute('src')) img.setAttribute('src', phImg);
|
|
119
|
+
img.onerror = () => {{ img.setAttribute('src', phImg); }};
|
|
120
|
+
}});
|
|
121
|
+
document.querySelectorAll('[data-bg-topic]').forEach(el => {{
|
|
122
|
+
const cs = window.getComputedStyle(el);
|
|
123
|
+
const hasBg = cs.backgroundImage && cs.backgroundImage !== 'none';
|
|
124
|
+
if (!hasBg) {{
|
|
125
|
+
el.style.backgroundImage = 'linear-gradient(135deg, rgba(13,110,253,.15) 0%, rgba(102,16,242,.15) 100%)';
|
|
126
|
+
el.style.backgroundSize = 'cover';
|
|
127
|
+
el.style.backgroundPosition = 'center';
|
|
128
|
+
if (!el.style.minHeight) el.style.minHeight = '220px';
|
|
129
|
+
}}
|
|
130
|
+
}});
|
|
131
|
+
|
|
132
|
+
// 视差滚动(基于 data-parallax)
|
|
133
|
+
const parallaxNodes = Array.from(document.querySelectorAll('[data-parallax]'));
|
|
134
|
+
if (parallaxNodes.length) {{
|
|
135
|
+
const updateParallax = () => {{
|
|
136
|
+
const scrollY = window.scrollY || window.pageYOffset;
|
|
137
|
+
parallaxNodes.forEach(node => {{
|
|
138
|
+
const speed = parseFloat(node.getAttribute('data-parallax')) || 0.18;
|
|
139
|
+
const rect = node.getBoundingClientRect();
|
|
140
|
+
const offset = (scrollY + rect.top) * speed * -1;
|
|
141
|
+
node.style.setProperty('--parallax-offset', `${{offset.toFixed(2)}}px`);
|
|
142
|
+
}});
|
|
143
|
+
}};
|
|
144
|
+
updateParallax();
|
|
145
|
+
window.addEventListener('scroll', () => requestAnimationFrame(updateParallax), {{ passive: true }});
|
|
146
|
+
window.addEventListener('resize', () => requestAnimationFrame(updateParallax));
|
|
147
|
+
}}
|
|
148
|
+
|
|
149
|
+
// 轻量 3D 倾斜效果(data-tilt)
|
|
150
|
+
const tiltNodes = Array.from(document.querySelectorAll('[data-tilt]'));
|
|
151
|
+
tiltNodes.forEach(node => {{
|
|
152
|
+
const strength = parseFloat(node.getAttribute('data-tilt-strength')) || 10;
|
|
153
|
+
const damp = parseFloat(node.getAttribute('data-tilt-damping')) || 0.12;
|
|
154
|
+
let currentX = 0;
|
|
155
|
+
let currentY = 0;
|
|
156
|
+
let frame;
|
|
157
|
+
|
|
158
|
+
const animate = () => {{
|
|
159
|
+
node.style.setProperty('--tilt-rotate-x', `${{currentY.toFixed(3)}}deg`);
|
|
160
|
+
node.style.setProperty('--tilt-rotate-y', `${{currentX.toFixed(3)}}deg`);
|
|
161
|
+
frame = requestAnimationFrame(animate);
|
|
162
|
+
}};
|
|
163
|
+
|
|
164
|
+
const handlePointerMove = (event) => {{
|
|
165
|
+
const rect = node.getBoundingClientRect();
|
|
166
|
+
const relX = (event.clientX - rect.left) / rect.width;
|
|
167
|
+
const relY = (event.clientY - rect.top) / rect.height;
|
|
168
|
+
const targetX = (relX - 0.5) * strength;
|
|
169
|
+
const targetY = (0.5 - relY) * strength;
|
|
170
|
+
currentX = currentX + (targetX - currentX) * damp;
|
|
171
|
+
currentY = currentY + (targetY - currentY) * damp;
|
|
172
|
+
}};
|
|
173
|
+
|
|
174
|
+
const resetTilt = () => {{
|
|
175
|
+
currentX = 0;
|
|
176
|
+
currentY = 0;
|
|
177
|
+
}};
|
|
178
|
+
|
|
179
|
+
node.addEventListener('pointerenter', () => {{
|
|
180
|
+
cancelAnimationFrame(frame);
|
|
181
|
+
frame = requestAnimationFrame(animate);
|
|
182
|
+
}});
|
|
183
|
+
node.addEventListener('pointermove', handlePointerMove);
|
|
184
|
+
node.addEventListener('pointerleave', () => {{
|
|
185
|
+
resetTilt();
|
|
186
|
+
cancelAnimationFrame(frame);
|
|
187
|
+
frame = requestAnimationFrame(animate);
|
|
188
|
+
setTimeout(() => cancelAnimationFrame(frame), 180);
|
|
189
|
+
}});
|
|
190
|
+
node.addEventListener('pointerup', resetTilt);
|
|
191
|
+
}});
|
|
192
|
+
|
|
193
|
+
// 噪点背景(data-canvas="noise")
|
|
194
|
+
const noiseCanvas = document.querySelector('[data-canvas="noise"]');
|
|
195
|
+
if (noiseCanvas) {{
|
|
196
|
+
const ctx = noiseCanvas.getContext('2d');
|
|
197
|
+
const renderNoise = () => {{
|
|
198
|
+
const {{ width, height }} = noiseCanvas.parentElement.getBoundingClientRect();
|
|
199
|
+
noiseCanvas.width = Math.max(320, Math.round(width));
|
|
200
|
+
noiseCanvas.height = Math.max(320, Math.round(height));
|
|
201
|
+
const imageData = ctx.createImageData(noiseCanvas.width, noiseCanvas.height);
|
|
202
|
+
const buffer = imageData.data;
|
|
203
|
+
for (let i = 0; i < buffer.length; i += 4) {{
|
|
204
|
+
const value = Math.random() * 255;
|
|
205
|
+
buffer[i] = value;
|
|
206
|
+
buffer[i + 1] = value;
|
|
207
|
+
buffer[i + 2] = value;
|
|
208
|
+
buffer[i + 3] = Math.random() * 50 + 30;
|
|
209
|
+
}}
|
|
210
|
+
ctx.putImageData(imageData, 0, 0);
|
|
211
|
+
}};
|
|
212
|
+
renderNoise();
|
|
213
|
+
window.addEventListener('resize', () => requestAnimationFrame(renderNoise));
|
|
214
|
+
}}
|
|
215
|
+
|
|
216
|
+
{content if content else '// 在这里添加你的JavaScript代码'}
|
|
217
|
+
}});
|
|
218
|
+
|
|
219
|
+
// 工具函数
|
|
220
|
+
function $(selector) {{ return document.querySelector(selector); }}
|
|
221
|
+
function $$(selector) {{ return document.querySelectorAll(selector); }}
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
# 确保父目录存在
|
|
226
|
+
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
228
|
+
f.write(js_template)
|
|
229
|
+
return f"JavaScript文件创建成功: {file_path}"
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise RuntimeError(f"创建JavaScript文件失败: {str(e)}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
__all__ = ["create_js_file"]
|