Initial commit: 国土空间规划课程智能体 v1.0
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
@@ -0,0 +1,394 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>p5.js 城市数据可视化</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.4/lib/p5.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; background: #0a0a1a; }
|
||||
canvas { display: block; }
|
||||
#info {
|
||||
position: fixed; bottom: 20px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="info">移动鼠标查看区域数据 | 点击切换数据维度 | 按空格切换视图</div>
|
||||
<script>
|
||||
let buildings = [];
|
||||
let roads = [];
|
||||
let districts = [];
|
||||
let currentMetric = 0;
|
||||
let metricNames = ['人口密度', '绿化率', '容积率', '交通便利度'];
|
||||
let metricColors = [
|
||||
[255, 100, 100],
|
||||
[100, 220, 100],
|
||||
[100, 150, 255],
|
||||
[255, 220, 80]
|
||||
];
|
||||
let camX = 0, camY = 0;
|
||||
let targetCamX = 0, targetCamY = 0;
|
||||
let hoveredBuilding = null;
|
||||
let viewMode = 0;
|
||||
let time = 0;
|
||||
let particles = [];
|
||||
|
||||
function setup() {
|
||||
createCanvas(windowWidth, windowHeight);
|
||||
generateCity();
|
||||
}
|
||||
|
||||
function generateCity() {
|
||||
buildings = [];
|
||||
roads = [];
|
||||
districts = [];
|
||||
particles = [];
|
||||
|
||||
// 生成道路网格
|
||||
let gridSize = 100;
|
||||
for (let x = -800; x <= 800; x += gridSize) {
|
||||
roads.push({ x1: x, y1: -800, x2: x, y2: 800 });
|
||||
}
|
||||
for (let y = -800; y <= 800; y += gridSize) {
|
||||
roads.push({ x1: -800, y1: y, x2: 800, y2: y });
|
||||
}
|
||||
|
||||
// 生成主路(更宽)
|
||||
for (let x = -800; x <= 800; x += gridSize * 3) {
|
||||
roads.push({ x1: x, y1: -800, x2: x, y2: 800, main: true });
|
||||
}
|
||||
for (let y = -800; y <= 800; y += gridSize * 3) {
|
||||
roads.push({ x1: -800, y1: y, x2: 800, y2: y, main: true });
|
||||
}
|
||||
|
||||
// 生成建筑
|
||||
let types = ['residential', 'commercial', 'industrial', 'park'];
|
||||
let typeLabels = { residential: '住宅区', commercial: '商业区', industrial: '工业区', park: '公园绿地' };
|
||||
|
||||
for (let gx = -750; gx < 750; gx += gridSize) {
|
||||
for (let gy = -750; gy < 750; gy += gridSize) {
|
||||
let cx = gx + gridSize / 2;
|
||||
let cy = gy + gridSize / 2;
|
||||
let dist = sqrt(cx * cx + cy * cy);
|
||||
let type;
|
||||
|
||||
if (dist < 200) type = 'commercial';
|
||||
else if (dist < 500) type = random() > 0.2 ? 'residential' : 'park';
|
||||
else type = random() > 0.3 ? 'residential' : 'industrial';
|
||||
|
||||
if (type === 'park') {
|
||||
buildings.push({
|
||||
x: cx, y: cy,
|
||||
w: gridSize * 0.8, h: gridSize * 0.8,
|
||||
height: 0, type: type, label: typeLabels[type],
|
||||
metrics: {
|
||||
pop: random(5, 20),
|
||||
green: random(70, 95),
|
||||
far: random(0.1, 0.3),
|
||||
traffic: random(20, 40)
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let count = floor(random(1, 5));
|
||||
for (let i = 0; i < count; i++) {
|
||||
let bw = random(15, gridSize * 0.4);
|
||||
let bh = random(15, gridSize * 0.4);
|
||||
let bx = cx + random(-gridSize * 0.3, gridSize * 0.3);
|
||||
let by = cy + random(-gridSize * 0.3, gridSize * 0.3);
|
||||
let bHeight;
|
||||
|
||||
if (type === 'commercial') bHeight = random(60, 180);
|
||||
else if (type === 'residential') bHeight = random(20, 80);
|
||||
else bHeight = random(15, 40);
|
||||
|
||||
buildings.push({
|
||||
x: bx, y: by,
|
||||
w: bw, h: bh,
|
||||
height: bHeight,
|
||||
type: type,
|
||||
label: typeLabels[type],
|
||||
metrics: {
|
||||
pop: type === 'commercial' ? random(200, 800) : random(50, 300),
|
||||
green: random(10, 40),
|
||||
far: bHeight / 50,
|
||||
traffic: random(30, 90)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 流动粒子(代表交通)
|
||||
for (let i = 0; i < 200; i++) {
|
||||
particles.push({
|
||||
roadIdx: floor(random(roads.length)),
|
||||
t: random(1),
|
||||
speed: random(0.002, 0.008),
|
||||
color: [255, 220, 80, random(100, 200)]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(10, 10, 26);
|
||||
time += 0.01;
|
||||
|
||||
// 平滑相机
|
||||
camX = lerp(camX, targetCamX, 0.05);
|
||||
camY = lerp(camY, targetCamY, 0.05);
|
||||
|
||||
push();
|
||||
translate(width / 2 + camX, height / 2 + camY);
|
||||
|
||||
// 绘制地面
|
||||
fill(15, 15, 35);
|
||||
noStroke();
|
||||
rect(-800, -800, 1600, 1600);
|
||||
|
||||
// 绘制道路
|
||||
for (let r of roads) {
|
||||
if (r.main) {
|
||||
stroke(40, 40, 70);
|
||||
strokeWeight(4);
|
||||
} else {
|
||||
stroke(30, 30, 55);
|
||||
strokeWeight(1);
|
||||
}
|
||||
line(r.x1, r.y1, r.x2, r.y2);
|
||||
}
|
||||
|
||||
// 更新和绘制流动粒子
|
||||
noStroke();
|
||||
for (let p of particles) {
|
||||
p.t += p.speed;
|
||||
if (p.t > 1) p.t -= 1;
|
||||
|
||||
let r = roads[p.roadIdx];
|
||||
let px = lerp(r.x1, r.x2, p.t);
|
||||
let py = lerp(r.y1, r.y2, p.t);
|
||||
fill(p.color);
|
||||
ellipse(px, py, 3, 3);
|
||||
}
|
||||
|
||||
hoveredBuilding = null;
|
||||
|
||||
// 绘制建筑
|
||||
for (let b of buildings) {
|
||||
let mc = metricColors[currentMetric];
|
||||
let metricVal;
|
||||
if (currentMetric === 0) metricVal = b.metrics.pop / 800;
|
||||
else if (currentMetric === 1) metricVal = b.metrics.green / 95;
|
||||
else if (currentMetric === 2) metricVal = b.metrics.far / 3.6;
|
||||
else metricVal = b.metrics.traffic / 90;
|
||||
metricVal = constrain(metricVal, 0, 1);
|
||||
|
||||
if (viewMode === 0) {
|
||||
drawBuildingTop(b, mc, metricVal);
|
||||
} else {
|
||||
drawBuildingIso(b, mc, metricVal);
|
||||
}
|
||||
|
||||
// 检测鼠标悬停
|
||||
let mx = mouseX - width / 2 - camX;
|
||||
let my = mouseY - height / 2 - camY;
|
||||
if (abs(mx - b.x) < b.w / 2 + 5 && abs(my - b.y) < b.h / 2 + 5) {
|
||||
hoveredBuilding = b;
|
||||
}
|
||||
}
|
||||
|
||||
pop();
|
||||
|
||||
// 绘制UI
|
||||
drawUI();
|
||||
|
||||
// 绘制悬停信息
|
||||
if (hoveredBuilding) {
|
||||
drawTooltip(hoveredBuilding);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBuildingTop(b, mc, val) {
|
||||
if (b.type === 'park') {
|
||||
fill(30, 100 + val * 80, 30, 150);
|
||||
noStroke();
|
||||
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 4);
|
||||
// 树
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let tx = b.x + random(-b.w * 0.3, b.w * 0.3);
|
||||
let ty = b.y + random(-b.h * 0.3, b.h * 0.3);
|
||||
fill(40, 140, 40, 180);
|
||||
ellipse(tx, ty, 8, 8);
|
||||
}
|
||||
} else {
|
||||
let pulse = sin(time * 2 + b.x * 0.01) * 0.1 + 0.9;
|
||||
let alpha = 80 + val * 170;
|
||||
fill(mc[0], mc[1], mc[2], alpha * pulse);
|
||||
noStroke();
|
||||
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 2);
|
||||
|
||||
// 高度阴影
|
||||
fill(0, 0, 0, b.height * 0.3);
|
||||
let offset = b.height * 0.1;
|
||||
rect(b.x - b.w / 2 + offset, b.y - b.h / 2 + offset, b.w, b.h, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBuildingIso(b, mc, val) {
|
||||
let h = b.height * 0.3;
|
||||
if (b.type === 'park') {
|
||||
fill(30, 120, 30, 150);
|
||||
noStroke();
|
||||
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
let alpha = 100 + val * 155;
|
||||
|
||||
// 右侧面
|
||||
fill(mc[0] * 0.6, mc[1] * 0.6, mc[2] * 0.6, alpha);
|
||||
noStroke();
|
||||
beginShape();
|
||||
vertex(b.x + b.w / 2, b.y - b.h / 2);
|
||||
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
|
||||
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
|
||||
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 + h * 0.4);
|
||||
endShape(CLOSE);
|
||||
|
||||
// 正面
|
||||
fill(mc[0] * 0.8, mc[1] * 0.8, mc[2] * 0.8, alpha);
|
||||
beginShape();
|
||||
vertex(b.x - b.w / 2, b.y - b.h / 2);
|
||||
vertex(b.x - b.w / 2, b.y - b.h / 2 - h);
|
||||
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
|
||||
vertex(b.x + b.w / 2, b.y - b.h / 2);
|
||||
endShape(CLOSE);
|
||||
|
||||
// 顶面
|
||||
fill(mc[0], mc[1], mc[2], alpha);
|
||||
beginShape();
|
||||
vertex(b.x - b.w / 2, b.y - b.h / 2 - h);
|
||||
vertex(b.x - b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
|
||||
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
|
||||
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
|
||||
endShape(CLOSE);
|
||||
}
|
||||
|
||||
function drawUI() {
|
||||
// 左上角标题
|
||||
fill(255, 255, 255, 200);
|
||||
noStroke();
|
||||
textSize(18);
|
||||
textAlign(LEFT, TOP);
|
||||
textFont('system-ui');
|
||||
text('城市数据可视化', 20, 20);
|
||||
|
||||
textSize(12);
|
||||
fill(255, 255, 255, 120);
|
||||
text('当前指标: ' + metricNames[currentMetric], 20, 48);
|
||||
|
||||
// 图例
|
||||
let legendY = 80;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let mc = metricColors[i];
|
||||
if (i === currentMetric) {
|
||||
fill(mc[0], mc[1], mc[2], 255);
|
||||
rect(20, legendY, 12, 12, 2);
|
||||
fill(255);
|
||||
textSize(13);
|
||||
text(metricNames[i], 38, legendY);
|
||||
} else {
|
||||
fill(mc[0], mc[1], mc[2], 80);
|
||||
rect(20, legendY, 12, 12, 2);
|
||||
fill(150);
|
||||
textSize(13);
|
||||
text(metricNames[i], 38, legendY);
|
||||
}
|
||||
legendY += 22;
|
||||
}
|
||||
|
||||
// 色阶条
|
||||
let barX = 20, barY = height - 50, barW = 200, barH = 12;
|
||||
let mc = metricColors[currentMetric];
|
||||
for (let i = 0; i < barW; i++) {
|
||||
let t = i / barW;
|
||||
fill(mc[0] * t, mc[1] * t, mc[2] * t);
|
||||
noStroke();
|
||||
rect(barX + i, barY, 1, barH);
|
||||
}
|
||||
fill(255, 255, 255, 120);
|
||||
textSize(11);
|
||||
text('低', barX, barY + barH + 14);
|
||||
textAlign(RIGHT, TOP);
|
||||
text('高', barX + barW, barY + barH + 14);
|
||||
|
||||
// 建筑数量
|
||||
textAlign(RIGHT, TOP);
|
||||
fill(255, 255, 255, 100);
|
||||
textSize(12);
|
||||
text('建筑: ' + buildings.length + ' | 粒子: ' + particles.length, width - 20, 20);
|
||||
text('视图: ' + (viewMode === 0 ? '俯视图' : '等轴测'), width - 20, 38);
|
||||
}
|
||||
|
||||
function drawTooltip(b) {
|
||||
let tx = mouseX + 15;
|
||||
let ty = mouseY - 10;
|
||||
|
||||
let tw = 160;
|
||||
let th = 100;
|
||||
|
||||
if (tx + tw > width) tx = mouseX - tw - 15;
|
||||
if (ty + th > height) ty = mouseY - th - 10;
|
||||
|
||||
fill(20, 20, 40, 220);
|
||||
stroke(80, 80, 120);
|
||||
strokeWeight(1);
|
||||
rect(tx, ty, tw, th, 6);
|
||||
|
||||
noStroke();
|
||||
fill(255);
|
||||
textSize(13);
|
||||
textAlign(LEFT, TOP);
|
||||
text(b.label, tx + 10, ty + 8);
|
||||
|
||||
fill(180);
|
||||
textSize(11);
|
||||
let keys = ['人口密度', '绿化率', '容积率', '交通便利'];
|
||||
let vals = [b.metrics.pop, b.metrics.green, b.metrics.far, b.metrics.traffic];
|
||||
let units = ['人/hm²', '%', '', '分'];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let mc = metricColors[i];
|
||||
fill(mc[0], mc[1], mc[2], i === currentMetric ? 255 : 120);
|
||||
text(keys[i] + ': ' + (i === 1 ? vals[i].toFixed(0) : vals[i].toFixed(1)) + units[i], tx + 10, ty + 30 + i * 17);
|
||||
}
|
||||
}
|
||||
|
||||
function mousePressed() {
|
||||
currentMetric = (currentMetric + 1) % 4;
|
||||
}
|
||||
|
||||
function keyPressed() {
|
||||
if (key === ' ') {
|
||||
viewMode = (viewMode + 1) % 2;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseDragged() {
|
||||
targetCamX += mouseX - pmouseX;
|
||||
targetCamY += mouseY - pmouseY;
|
||||
}
|
||||
|
||||
function windowResized() {
|
||||
resizeCanvas(windowWidth, windowHeight);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>国土空间规划课程智能体</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.4/lib/p5.min.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { overflow: hidden; background: #0a0a1a; font-family: 'Noto Sans SC', system-ui, sans-serif; }
|
||||
canvas { display: block; position: fixed; top: 0; left: 0; z-index: 0; }
|
||||
#overlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
z-index: 1; pointer-events: none;
|
||||
display: flex; flex-direction: column; justify-content: center; align-items: center;
|
||||
}
|
||||
#title {
|
||||
font-size: 48px; font-weight: 700; color: white;
|
||||
text-shadow: 0 0 40px rgba(100, 140, 255, 0.4);
|
||||
margin-bottom: 12px; letter-spacing: 4px;
|
||||
}
|
||||
#subtitle {
|
||||
font-size: 18px; font-weight: 300; color: rgba(180, 200, 255, 0.7);
|
||||
margin-bottom: 40px; letter-spacing: 2px;
|
||||
}
|
||||
#tagline {
|
||||
font-size: 14px; color: rgba(140, 160, 220, 0.5);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
#info {
|
||||
position: fixed; bottom: 20px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-size: 12px; z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="overlay">
|
||||
<div id="title">国土空间规划课程智能体</div>
|
||||
<div id="subtitle">基于大模型的智能问答系统</div>
|
||||
<div id="tagline">哈尔滨工业大学建筑与设计学院</div>
|
||||
</div>
|
||||
<div id="info">移动鼠标与知识网络互动</div>
|
||||
<script>
|
||||
const keywords = [
|
||||
'容积率', '建筑密度', '绿地率', '控规', '总规', '详规',
|
||||
'用地性质', '红线', '蓝线', '绿线', '紫线', '黄线',
|
||||
'城市更新', '国土空间', '双评价', '三区三线',
|
||||
'生态保护', '耕地保护', '城镇开发', '用途管制',
|
||||
'空间结构', '交通规划', '基础设施', '公共服务',
|
||||
'人口预测', '用地布局', '天际线', '风环境',
|
||||
'海绵城市', '韧性城市', '智慧城市', '碳中和'
|
||||
];
|
||||
|
||||
let nodes = [];
|
||||
let nodeCount;
|
||||
let connectDist = 150;
|
||||
|
||||
function setup() {
|
||||
createCanvas(windowWidth, windowHeight);
|
||||
nodeCount = min(floor(width * height / 12000), 80);
|
||||
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
nodes.push(createNode());
|
||||
}
|
||||
}
|
||||
|
||||
function createNode() {
|
||||
let kw = random(keywords);
|
||||
return {
|
||||
x: random(width),
|
||||
y: random(height),
|
||||
vx: random(-0.3, 0.3),
|
||||
vy: random(-0.3, 0.3),
|
||||
size: textWidth(kw) || kw.length * 14,
|
||||
baseSize: random(3, 6),
|
||||
keyword: kw,
|
||||
showText: random() > 0.5,
|
||||
hue: random(200, 280),
|
||||
phase: random(TWO_PI),
|
||||
textAlpha: random() > 0.6 ? random(60, 150) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(10, 10, 26, 30);
|
||||
|
||||
// 渐变背景(每帧微量覆盖)
|
||||
noStroke();
|
||||
fill(10, 10, 26, 25);
|
||||
rect(0, 0, width, height);
|
||||
|
||||
// 绘制连线
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
let d = dist(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
|
||||
if (d < connectDist) {
|
||||
let alpha = map(d, 0, connectDist, 60, 0);
|
||||
let h = (nodes[i].hue + nodes[j].hue) / 2;
|
||||
stroke(h, 60, 200, alpha);
|
||||
strokeWeight(0.8);
|
||||
line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标吸引线
|
||||
for (let n of nodes) {
|
||||
let d = dist(mouseX, mouseY, n.x, n.y);
|
||||
if (d < 200) {
|
||||
let alpha = map(d, 0, 200, 80, 0);
|
||||
stroke(220, 180, 255, alpha);
|
||||
strokeWeight(1);
|
||||
line(mouseX, mouseY, n.x, n.y);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新和绘制节点
|
||||
for (let n of nodes) {
|
||||
// 鼠标交互
|
||||
let dm = dist(mouseX, mouseY, n.x, n.y);
|
||||
if (dm < 180) {
|
||||
let force = map(dm, 0, 180, 0.5, 0);
|
||||
let angle = atan2(n.y - mouseY, n.x - mouseX);
|
||||
n.vx += cos(angle) * force * 0.05;
|
||||
n.vy += sin(angle) * force * 0.05;
|
||||
}
|
||||
|
||||
// 运动
|
||||
n.x += n.vx;
|
||||
n.y += n.vy;
|
||||
|
||||
// 摩擦力
|
||||
n.vx *= 0.99;
|
||||
n.vy *= 0.99;
|
||||
|
||||
// 添加微小随机运动
|
||||
n.vx += random(-0.02, 0.02);
|
||||
n.vy += random(-0.02, 0.02);
|
||||
|
||||
// 边界
|
||||
if (n.x < 0) { n.x = 0; n.vx *= -1; }
|
||||
if (n.x > width) { n.x = width; n.vx *= -1; }
|
||||
if (n.y < 0) { n.y = 0; n.vy *= -1; }
|
||||
if (n.y > height) { n.y = height; n.vy *= -1; }
|
||||
|
||||
// 脉动效果
|
||||
let pulse = sin(frameCount * 0.02 + n.phase) * 0.3 + 0.7;
|
||||
|
||||
// 绘制光晕
|
||||
noStroke();
|
||||
let glowAlpha = 20 * pulse;
|
||||
fill(n.hue, 50, 200, glowAlpha);
|
||||
ellipse(n.x, n.y, n.baseSize * 8, n.baseSize * 8);
|
||||
|
||||
fill(n.hue, 50, 200, glowAlpha * 1.5);
|
||||
ellipse(n.x, n.y, n.baseSize * 4, n.baseSize * 4);
|
||||
|
||||
// 绘制节点
|
||||
let nodeAlpha = (dm < 180) ? map(dm, 0, 180, 255, 120) : 120 * pulse;
|
||||
fill(n.hue, 60, 240, nodeAlpha);
|
||||
ellipse(n.x, n.y, n.baseSize * 2, n.baseSize * 2);
|
||||
|
||||
// 绘制关键词文字
|
||||
if (n.textAlpha > 0) {
|
||||
let ta = n.textAlpha * pulse;
|
||||
if (dm < 180) ta = min(ta + 80, 220);
|
||||
fill(n.hue, 30, 230, ta);
|
||||
noStroke();
|
||||
textAlign(CENTER, CENTER);
|
||||
textSize(12);
|
||||
textFont('Noto Sans SC');
|
||||
text(n.keyword, n.x, n.y - n.baseSize * 2 - 8);
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标光标光晕
|
||||
noStroke();
|
||||
fill(230, 200, 255, 8);
|
||||
ellipse(mouseX, mouseY, 200, 200);
|
||||
fill(230, 200, 255, 15);
|
||||
ellipse(mouseX, mouseY, 80, 80);
|
||||
fill(230, 200, 255, 30);
|
||||
ellipse(mouseX, mouseY, 20, 20);
|
||||
}
|
||||
|
||||
function windowResized() {
|
||||
resizeCanvas(windowWidth, windowHeight);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>p5.js 互动图形 Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.4/lib/p5.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; background: #000; }
|
||||
canvas { display: block; }
|
||||
#info {
|
||||
position: fixed; bottom: 20px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="info">移动鼠标生成图形 | 点击产生爆炸 | 按空格清空画布</div>
|
||||
<script>
|
||||
let shapes = [];
|
||||
let hueOffset = 0;
|
||||
|
||||
function setup() {
|
||||
createCanvas(windowWidth, windowHeight);
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
rectMode(CENTER);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(0, 0, 0, 5);
|
||||
|
||||
for (let i = shapes.length - 1; i >= 0; i--) {
|
||||
let s = shapes[i];
|
||||
s.angle += s.rotSpeed;
|
||||
s.size += s.growSpeed;
|
||||
s.alpha -= s.fadeSpeed;
|
||||
|
||||
if (s.alpha <= 0) {
|
||||
shapes.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
push();
|
||||
translate(s.x, s.y);
|
||||
rotate(s.angle);
|
||||
noStroke();
|
||||
fill(s.hue, s.sat, s.bright, s.alpha);
|
||||
|
||||
if (s.type === 0) {
|
||||
ellipse(0, 0, s.size, s.size);
|
||||
} else if (s.type === 1) {
|
||||
triangle(
|
||||
0, -s.size / 2,
|
||||
-s.size / 2, s.size / 2,
|
||||
s.size / 2, s.size / 2
|
||||
);
|
||||
} else {
|
||||
rect(0, 0, s.size, s.size);
|
||||
}
|
||||
pop();
|
||||
}
|
||||
|
||||
hueOffset = (hueOffset + 0.5) % 360;
|
||||
}
|
||||
|
||||
function mouseMoved() {
|
||||
let hue = (hueOffset + random(-20, 20) + 360) % 360;
|
||||
shapes.push({
|
||||
x: mouseX + random(-15, 15),
|
||||
y: mouseY + random(-15, 15),
|
||||
size: random(10, 30),
|
||||
angle: random(TWO_PI),
|
||||
rotSpeed: random(-0.05, 0.05),
|
||||
growSpeed: random(0.3, 1.2),
|
||||
fadeSpeed: random(0.8, 2.0),
|
||||
alpha: 80,
|
||||
hue: hue,
|
||||
sat: random(60, 90),
|
||||
bright: random(70, 100),
|
||||
type: floor(random(3))
|
||||
});
|
||||
|
||||
if (shapes.length > 500) {
|
||||
shapes.splice(0, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function mousePressed() {
|
||||
let count = floor(random(15, 25));
|
||||
for (let i = 0; i < count; i++) {
|
||||
let angle = random(TWO_PI);
|
||||
let dist = random(20, 120);
|
||||
let hue = (hueOffset + random(-40, 40) + 360) % 360;
|
||||
shapes.push({
|
||||
x: mouseX + cos(angle) * dist,
|
||||
y: mouseY + sin(angle) * dist,
|
||||
size: random(8, 25),
|
||||
angle: angle,
|
||||
rotSpeed: random(-0.08, 0.08),
|
||||
growSpeed: random(0.5, 2.0),
|
||||
fadeSpeed: random(1.0, 2.5),
|
||||
alpha: 90,
|
||||
hue: hue,
|
||||
sat: random(70, 100),
|
||||
bright: random(80, 100),
|
||||
type: floor(random(3))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function keyPressed() {
|
||||
if (key === ' ') {
|
||||
shapes = [];
|
||||
background(0);
|
||||
}
|
||||
}
|
||||
|
||||
function windowResized() {
|
||||
resizeCanvas(windowWidth, windowHeight);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user