refactor: remove weather anim, rewrite AI prompt with graded severity and 6-point fix
This commit is contained in:
+23
-21
@@ -24,46 +24,48 @@ def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) ->
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
prompt = f"""
|
||||
你是一个专业的天气衍生品(如 Polymarket)交易员。你的任务是分析当前天气特征,判断今日实测最高温是否能达到或超过预报中的【最高值】。
|
||||
你是一个专业的天气衍生品交易员。你的任务是分析当前气象实况,判断今日实测最高温的结算落点。
|
||||
结算基准为合约指定源(通常为 METAR 口径整数四舍五入),以下用"结算值"代替具体源名称。
|
||||
|
||||
请综合以下提供的【{city_name}】气象特征进行深度推理。
|
||||
|
||||
【气象特征与事实】
|
||||
{weather_insights}
|
||||
|
||||
【分析优先级】(按此顺序逐项检查,前置项可推翻后置项的结论)
|
||||
【分析框架】(按此顺序逐项检查,前置项可约束后置项的结论)
|
||||
|
||||
P0 **预报崩盘检测**(最最高优先级):
|
||||
- 如果数据中出现"🚨 预报崩盘"标记,说明所有模型集体严重高估。
|
||||
- 此时你必须:① 直接宣布预报失准/崩盘;② 以实测最高温为结算锚点;③ 分析可能导致崩盘的原因(冷平流、降水、云量压制等);④ 禁止再讨论"冲击"预报值。
|
||||
- 如果峰值窗口已过且实测远低于预报,结算已基本确定,应直接给出结算值。
|
||||
P0 **预报失准检测**:
|
||||
- 若数据含"🚨 预报崩盘"或"⚠️ 预报差距"标记,判定预报失准。
|
||||
- 失准等级:轻(偏差2-3°) / 中(3-5°) / 重(>5°)。
|
||||
- 但"失准"≠"已定局":还需检查近2报斜率是否≤0 且 风向/云量不支持二次抬升,才能判定结算锁定。
|
||||
- 若斜率仍>0或有暖平流迹象,应注明"预报偏高但仍有上行空间"。
|
||||
|
||||
P1 **实况节奏**:
|
||||
- 近2-4条METAR的温度走势:连涨/持平/回落?
|
||||
- 触发阈值:连续2报创新高 → 升温未止;连续2报未创新高且已过峰值窗 → 偏死盘。
|
||||
- 低辐射时段(<100W/m²)仍在升温 → 暖平流驱动,预报往往低估。
|
||||
- 连续2报创新高 → 升温未止;连续2报未创新高且斜率≤0 → 偏死盘。
|
||||
- 升温出现在低辐射时段 → 可能有多因子叠加(平流/混合层/热岛),不可单因子归因。
|
||||
|
||||
P2 **阻碍因子**:
|
||||
- 触发阈值:湿度>80% 且 云量BKN/OVC持续2报 → 判定压温有效。
|
||||
P2 **阻碍因子**(需结合城市特性判断):
|
||||
- 降水已出现(非trace) → 强压温。
|
||||
- 若阻碍条件未满足,不可凭"多云"单因子就判断"升温受限"。
|
||||
- 高湿度+厚云层持续2报以上 → 压温可能有效,但阈值因城市(海洋型 vs 大陆型)而异,不可套用固定数值。
|
||||
- 若仅单因子(如仅多云),不足以断定"升温受限"。
|
||||
|
||||
P3 **数学概率**:
|
||||
- 参考我提供的结算概率分布,但不可用概率去压过 P0/P1 实况。
|
||||
- 概率是辅助判据,实测趋势是主判据。
|
||||
P3 **概率与一致性校验**:
|
||||
- 参考结算概率分布,与 P1 实况做一致性检查。
|
||||
- 若概率分布与实况趋势矛盾,以实况为准并说明偏离原因。
|
||||
- 概率可辅助判断边界进位风险(如 X.5 线附近)。
|
||||
|
||||
P4 **预报背景**(最低优先级):
|
||||
- DEB融合值和预报可用于判断上沿空间和回落节奏。
|
||||
- 但当实测已远低于预报时,预报值已失去参考意义,不可继续引用。
|
||||
- 结算边界:温度处于 X.5 进位线附近时需特别预警。
|
||||
- 可参考 DEB/预报做上沿空间评估。
|
||||
- 当实测已显著偏离预报时,禁止继续引用预报值作为目标。
|
||||
|
||||
【输出要求】
|
||||
1. **禁止废话**,整体控制在 300 字以内。
|
||||
1. 正常场景控制在 300 字左右;异常场景(预报失准/极端走势)可扩展到 450 字。
|
||||
2. 严格按照以下 HTML 格式输出:
|
||||
|
||||
🤖 <b>Groq AI 决策</b>
|
||||
- 🎲 盘口: [若预报崩盘,直接给出"预报集体失准,WU结算锁定X°C"。否则:明确指出最热时段以及博弈焦点。区分"已确认WU底线 X°C"与"仍有概率冲击 Y°C"。禁止在升温未止时使用"锁定"等封顶措辞。若符合死盘条件,请直接给出死盘结论并说明理由。]
|
||||
- 💡 逻辑: [用 3-5 句话深度分析。如果预报崩盘,重点分析崩盘原因和实测偏差。如果正常,分析实测与预报的博弈和动力条件。需包含具体数值。]
|
||||
- 🎲 盘口: [给出结算判断。用"已确认底线 X{temp_symbol}"表示下限确定;用"上沿待确认,关注 Y{temp_symbol}"表示仍有变数。若预报严重失准,注明失准等级和原因。禁止在升温未止时用"锁定"。]
|
||||
- 💡 逻辑: [3-5 句深度分析。含具体数值。预报失准时重点分析偏差成因。正常时分析实测与预报的动态博弈。]
|
||||
- 🎯 置信度: [1-10]/10
|
||||
"""
|
||||
|
||||
@@ -86,7 +88,7 @@ P4 **预报背景**(最低优先级):
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 250,
|
||||
"max_tokens": 400,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
|
||||
+13
-4
@@ -502,18 +502,27 @@ def _analyze(city: str) -> Dict[str, Any]:
|
||||
)
|
||||
ai_parts.append(f"模型分歧: {mm_str}")
|
||||
|
||||
# --- Forecast bust detection for AI ---
|
||||
# --- Forecast miss severity for AI ---
|
||||
if forecast_miss_deg > 2.0 and peak_status in ("past", "in_window"):
|
||||
if forecast_miss_deg > 5.0:
|
||||
severity = "重"
|
||||
elif forecast_miss_deg > 3.0:
|
||||
severity = "中"
|
||||
else:
|
||||
severity = "轻"
|
||||
|
||||
min_forecast = min(
|
||||
(v for v in current_forecasts.values() if v is not None), default=None
|
||||
)
|
||||
slope_info = trend_info["direction"]
|
||||
ai_parts.append(
|
||||
f"🚨 预报崩盘: 所有模型集体高估!最低预报 {min_forecast}{sym} vs 实测最高 {max_so_far}{sym},"
|
||||
f"偏差 {forecast_miss_deg}°。已进入/过了峰值窗口,温度严重不达预期。"
|
||||
f"🚨 预报崩盘 [{severity}级失准]: 最低预报 {min_forecast}{sym} vs 实测最高 {max_so_far}{sym},"
|
||||
f"偏差 {forecast_miss_deg}°。当前趋势: {slope_info}。"
|
||||
)
|
||||
elif forecast_miss_deg > 4.0 and peak_status == "before":
|
||||
ai_parts.append(
|
||||
f"⚠️ 预报差距: 距峰值窗口尚有时间,但实测已落后预报 {forecast_miss_deg}°。"
|
||||
f"⚠️ 预报差距 [轻级]: 距峰值窗口尚有时间,但实测已落后预报 {forecast_miss_deg}°。"
|
||||
f"当前趋势: {trend_info['direction']}。"
|
||||
)
|
||||
|
||||
ai_context = "\n".join(ai_parts)
|
||||
|
||||
@@ -255,12 +255,6 @@ function renderPanel(data) {
|
||||
// Trigger reflow for animation
|
||||
requestAnimationFrame(() => panel.classList.add("visible"));
|
||||
|
||||
// Start weather animation
|
||||
if (typeof WeatherAnim !== "undefined") {
|
||||
// Small delay for canvas to be in DOM
|
||||
setTimeout(() => WeatherAnim.start(data), 100);
|
||||
}
|
||||
|
||||
// Header
|
||||
document.getElementById("panelCityName").textContent =
|
||||
`${data.risk?.emoji || "🏙️"} ${data.display_name}`;
|
||||
@@ -716,8 +710,6 @@ function closePanel() {
|
||||
document
|
||||
.querySelectorAll(".city-item")
|
||||
.forEach((el) => el.classList.remove("active"));
|
||||
// Stop weather animation
|
||||
if (typeof WeatherAnim !== "undefined") WeatherAnim.stop();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -63,11 +63,6 @@
|
||||
</div>
|
||||
|
||||
<div id="panelContent" class="panel-body">
|
||||
<!-- ── Weather Animation Overlay ── -->
|
||||
<div class="weather-anim-container" id="weatherAnimContainer">
|
||||
<canvas id="weatherCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- ── Temperature Hero ── -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-weather" id="heroWeather"></div>
|
||||
@@ -153,7 +148,6 @@
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
<script src="/static/weather-anim.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -495,81 +495,10 @@ body {
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* ── Weather Animation ── */
|
||||
.weather-anim-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
margin-bottom: -180px; /* Overlap with hero */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.weather-anim-container canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Night sky gradient for the animation background */
|
||||
.weather-anim-container.active {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(10, 14, 30, 0.9) 0%,
|
||||
rgba(15, 20, 40, 0.6) 40%,
|
||||
rgba(10, 14, 26, 0.3) 100%
|
||||
);
|
||||
}
|
||||
.weather-anim-container.clear {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(30, 58, 95, 0.5) 0%,
|
||||
rgba(15, 25, 50, 0.3) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.weather-anim-container.rain {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(30, 40, 60, 0.7) 0%,
|
||||
rgba(20, 30, 50, 0.4) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.weather-anim-container.snow {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(50, 60, 80, 0.6) 0%,
|
||||
rgba(30, 40, 60, 0.3) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.weather-anim-container.fog {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(100, 110, 130, 0.4) 0%,
|
||||
rgba(60, 70, 90, 0.2) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.weather-anim-container.storm {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(20, 20, 35, 0.85) 0%,
|
||||
rgba(15, 15, 30, 0.5) 60%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Hero Section ── */
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
padding-top: 12px !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-weather {
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// Weather Animation Engine
|
||||
// Canvas-based particle system for rain, snow, fog, etc.
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
const WeatherAnim = (() => {
|
||||
let canvas, ctx;
|
||||
let animId = null;
|
||||
let particles = [];
|
||||
let currentType = null;
|
||||
let lightningTimer = 0;
|
||||
let lightningFlash = 0;
|
||||
|
||||
const CONFIGS = {
|
||||
rain: {
|
||||
count: 120,
|
||||
init: (w, h) => ({
|
||||
x: Math.random() * w * 1.2 - w * 0.1,
|
||||
y: Math.random() * h - h,
|
||||
len: 12 + Math.random() * 18,
|
||||
speed: 8 + Math.random() * 6,
|
||||
opacity: 0.2 + Math.random() * 0.4,
|
||||
wind: 2 + Math.random() * 2,
|
||||
}),
|
||||
draw: (p, ctx) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(p.x + p.wind * 0.5, p.y + p.len);
|
||||
ctx.strokeStyle = `rgba(174, 214, 241, ${p.opacity})`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
},
|
||||
update: (p, w, h) => {
|
||||
p.y += p.speed;
|
||||
p.x += p.wind;
|
||||
if (p.y > h) {
|
||||
p.y = -p.len;
|
||||
p.x = Math.random() * w * 1.2 - w * 0.1;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
heavyrain: {
|
||||
count: 200,
|
||||
init: (w, h) => ({
|
||||
x: Math.random() * w * 1.3 - w * 0.15,
|
||||
y: Math.random() * h - h,
|
||||
len: 18 + Math.random() * 24,
|
||||
speed: 12 + Math.random() * 8,
|
||||
opacity: 0.3 + Math.random() * 0.5,
|
||||
wind: 4 + Math.random() * 3,
|
||||
}),
|
||||
draw: (p, ctx) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(p.x + p.wind * 0.6, p.y + p.len);
|
||||
ctx.strokeStyle = `rgba(160, 200, 230, ${p.opacity})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
},
|
||||
update: (p, w, h) => {
|
||||
p.y += p.speed;
|
||||
p.x += p.wind;
|
||||
if (p.y > h) {
|
||||
p.y = -p.len;
|
||||
p.x = Math.random() * w * 1.3 - w * 0.15;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
snow: {
|
||||
count: 80,
|
||||
init: (w, h) => ({
|
||||
x: Math.random() * w,
|
||||
y: Math.random() * h - h,
|
||||
r: 1.5 + Math.random() * 3,
|
||||
speed: 0.8 + Math.random() * 1.5,
|
||||
opacity: 0.4 + Math.random() * 0.4,
|
||||
wobble: Math.random() * Math.PI * 2,
|
||||
wobbleSpeed: 0.01 + Math.random() * 0.03,
|
||||
wobbleAmp: 0.5 + Math.random() * 1.5,
|
||||
}),
|
||||
draw: (p, ctx) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity})`;
|
||||
ctx.fill();
|
||||
},
|
||||
update: (p, w, h) => {
|
||||
p.y += p.speed;
|
||||
p.wobble += p.wobbleSpeed;
|
||||
p.x += Math.sin(p.wobble) * p.wobbleAmp;
|
||||
if (p.y > h + p.r) {
|
||||
p.y = -p.r * 2;
|
||||
p.x = Math.random() * w;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
fog: {
|
||||
count: 15,
|
||||
init: (w, h) => ({
|
||||
x: Math.random() * w,
|
||||
y: 20 + Math.random() * (h - 40),
|
||||
w: 120 + Math.random() * 200,
|
||||
h: 30 + Math.random() * 60,
|
||||
speed: 0.15 + Math.random() * 0.3,
|
||||
opacity: 0.03 + Math.random() * 0.08,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
}),
|
||||
draw: (p, ctx) => {
|
||||
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.w / 2);
|
||||
const osc = Math.sin(p.phase) * 0.02;
|
||||
grad.addColorStop(0, `rgba(200, 210, 230, ${p.opacity + osc})`);
|
||||
grad.addColorStop(1, "rgba(200, 210, 230, 0)");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(p.x - p.w / 2, p.y - p.h / 2, p.w, p.h);
|
||||
},
|
||||
update: (p, w) => {
|
||||
p.x += p.speed;
|
||||
p.phase += 0.01;
|
||||
if (p.x - p.w / 2 > w) {
|
||||
p.x = -p.w / 2;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
clear: {
|
||||
count: 0,
|
||||
init: () => ({}),
|
||||
draw: () => {},
|
||||
update: () => {},
|
||||
},
|
||||
|
||||
cloudy: {
|
||||
count: 8,
|
||||
init: (w, h) => ({
|
||||
x: Math.random() * w,
|
||||
y: 10 + Math.random() * 60,
|
||||
w: 100 + Math.random() * 160,
|
||||
h: 25 + Math.random() * 35,
|
||||
speed: 0.1 + Math.random() * 0.15,
|
||||
opacity: 0.04 + Math.random() * 0.06,
|
||||
}),
|
||||
draw: (p, ctx) => {
|
||||
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.w / 2);
|
||||
grad.addColorStop(0, `rgba(150, 160, 180, ${p.opacity})`);
|
||||
grad.addColorStop(1, "rgba(150, 160, 180, 0)");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(p.x - p.w / 2, p.y - p.h / 2, p.w, p.h);
|
||||
},
|
||||
update: (p, w) => {
|
||||
p.x += p.speed;
|
||||
if (p.x - p.w / 2 > w) p.x = -p.w / 2;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function detectWeatherType(data) {
|
||||
if (!data || !data.current) return "clear";
|
||||
const wx = (data.current.wx_desc || "").toUpperCase();
|
||||
const cloud = (data.current.cloud_desc || "").toLowerCase();
|
||||
|
||||
// Priority: precipitation > fog > clouds > clear
|
||||
if (wx.includes("TS") || wx.includes("TSRA")) return "storm";
|
||||
if (wx.includes("+RA") || wx.includes("+SN")) return "heavyrain";
|
||||
if (wx.includes("RA") || wx.includes("DZ") || wx.includes("SHRA"))
|
||||
return "rain";
|
||||
if (wx.includes("SN") || wx.includes("GR") || wx.includes("GS"))
|
||||
return "snow";
|
||||
if (wx.includes("FG") || wx.includes("BR") || wx.includes("HZ"))
|
||||
return "fog";
|
||||
|
||||
// Cloud-based
|
||||
if (cloud.includes("阴天") || cloud.includes("多云")) return "cloudy";
|
||||
|
||||
return "clear";
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
canvas = document.getElementById("weatherCanvas");
|
||||
if (!canvas) return false;
|
||||
const container = canvas.parentElement;
|
||||
canvas.width = container.offsetWidth * window.devicePixelRatio;
|
||||
canvas.height = container.offsetHeight * window.devicePixelRatio;
|
||||
canvas.style.width = container.offsetWidth + "px";
|
||||
canvas.style.height = container.offsetHeight + "px";
|
||||
ctx = canvas.getContext("2d");
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
return true;
|
||||
}
|
||||
|
||||
function createParticles(type) {
|
||||
const config = CONFIGS[type] || CONFIGS.clear;
|
||||
const container = canvas.parentElement;
|
||||
const w = container.offsetWidth;
|
||||
const h = container.offsetHeight;
|
||||
particles = [];
|
||||
for (let i = 0; i < config.count; i++) {
|
||||
particles.push(config.init(w, h));
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!ctx || !canvas) return;
|
||||
const container = canvas.parentElement;
|
||||
const w = container.offsetWidth;
|
||||
const h = container.offsetHeight;
|
||||
const config = CONFIGS[currentType] || CONFIGS.clear;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Lightning effect for storms
|
||||
if (currentType === "storm") {
|
||||
lightningTimer++;
|
||||
if (lightningTimer > 120 + Math.random() * 200) {
|
||||
lightningFlash = 6;
|
||||
lightningTimer = 0;
|
||||
}
|
||||
if (lightningFlash > 0) {
|
||||
const alpha = (lightningFlash / 6) * 0.25;
|
||||
ctx.fillStyle = `rgba(200, 220, 255, ${alpha})`;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
lightningFlash--;
|
||||
}
|
||||
|
||||
// Storm uses heavyrain particles
|
||||
const stormConfig = CONFIGS.heavyrain;
|
||||
particles.forEach((p) => {
|
||||
stormConfig.draw(p, ctx);
|
||||
stormConfig.update(p, w, h);
|
||||
});
|
||||
} else {
|
||||
particles.forEach((p) => {
|
||||
config.draw(p, ctx);
|
||||
config.update(p, w, h);
|
||||
});
|
||||
}
|
||||
|
||||
animId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function start(data) {
|
||||
stop();
|
||||
if (!initCanvas()) return;
|
||||
|
||||
const type = detectWeatherType(data);
|
||||
currentType = type;
|
||||
|
||||
// Set container CSS class for background gradient
|
||||
const container = document.getElementById("weatherAnimContainer");
|
||||
container.className = "weather-anim-container active " + type;
|
||||
|
||||
// Storm uses heavyrain particles config
|
||||
if (type === "storm") {
|
||||
const stormW = container.offsetWidth;
|
||||
const stormH = container.offsetHeight;
|
||||
particles = [];
|
||||
for (let i = 0; i < CONFIGS.heavyrain.count; i++) {
|
||||
particles.push(CONFIGS.heavyrain.init(stormW, stormH));
|
||||
}
|
||||
lightningTimer = 0;
|
||||
lightningFlash = 0;
|
||||
} else {
|
||||
createParticles(type);
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (animId) {
|
||||
cancelAnimationFrame(animId);
|
||||
animId = null;
|
||||
}
|
||||
particles = [];
|
||||
currentType = null;
|
||||
const container = document.getElementById("weatherAnimContainer");
|
||||
if (container) {
|
||||
container.className = "weather-anim-container";
|
||||
}
|
||||
}
|
||||
|
||||
return { start, stop, detectWeatherType };
|
||||
})();
|
||||
Reference in New Issue
Block a user