test.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>SVG Path 平滑曲线实现</title>
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  8. <style>
  9. * {
  10. margin: 0;
  11. padding: 0;
  12. box-sizing: border-box;
  13. font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
  14. }
  15. body {
  16. background: linear-gradient(135deg, #1a2a6c, #0d1b36);
  17. min-height: 100vh;
  18. display: flex;
  19. justify-content: center;
  20. align-items: center;
  21. padding: 20px;
  22. color: #fff;
  23. }
  24. .container {
  25. width: 100%;
  26. max-width: 900px;
  27. background: rgba(13, 27, 54, 0.8);
  28. border-radius: 20px;
  29. padding: 30px;
  30. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  31. border: 1px solid rgba(76, 201, 240, 0.3);
  32. }
  33. h1 {
  34. text-align: center;
  35. color: #4cc9f0;
  36. margin-bottom: 30px;
  37. text-shadow: 0 0 10px rgba(76, 201, 240, 0.5);
  38. }
  39. .chart-container {
  40. width: 100%;
  41. height: 400px;
  42. background: rgba(30, 45, 80, 0.5);
  43. border-radius: 10px;
  44. padding: 20px;
  45. margin-bottom: 30px;
  46. position: relative;
  47. }
  48. .chart {
  49. width: 100%;
  50. height: 100%;
  51. }
  52. .controls {
  53. display: flex;
  54. justify-content: center;
  55. gap: 15px;
  56. margin-bottom: 20px;
  57. }
  58. .control-btn {
  59. background: rgba(76, 201, 240, 0.3);
  60. border: none;
  61. padding: 10px 20px;
  62. border-radius: 5px;
  63. color: white;
  64. font-weight: bold;
  65. cursor: pointer;
  66. transition: all 0.3s;
  67. }
  68. .control-btn:hover {
  69. background: rgba(76, 201, 240, 0.5);
  70. }
  71. .control-btn.active {
  72. background: rgba(76, 201, 240, 0.7);
  73. box-shadow: 0 0 10px rgba(76, 201, 240, 0.5);
  74. }
  75. .code-section {
  76. background: rgba(0, 0, 0, 0.3);
  77. border-radius: 10px;
  78. padding: 20px;
  79. margin-top: 30px;
  80. }
  81. .code-title {
  82. color: #a5b4fc;
  83. margin-bottom: 15px;
  84. }
  85. .code-block {
  86. background: #1e1e1e;
  87. border-radius: 8px;
  88. padding: 15px;
  89. font-family: 'Courier New', monospace;
  90. overflow-x: auto;
  91. line-height: 1.5;
  92. }
  93. .comment {
  94. color: #6a9955;
  95. }
  96. .property {
  97. color: #9cdcfe;
  98. }
  99. .value {
  100. color: #ce9178;
  101. }
  102. .explanation {
  103. background: rgba(30, 30, 60, 0.5);
  104. padding: 15px;
  105. border-radius: 8px;
  106. margin-top: 20px;
  107. border-left: 4px solid #4cc9f0;
  108. }
  109. .explanation h3 {
  110. color: #a5b4fc;
  111. margin-bottom: 10px;
  112. }
  113. .point {
  114. fill: #4cc9f0;
  115. stroke: #fff;
  116. stroke-width: 2;
  117. cursor: pointer;
  118. transition: all 0.3s;
  119. }
  120. .point:hover {
  121. r: 8;
  122. fill: #ff6b6b;
  123. }
  124. .point-label {
  125. font-size: 0.8rem;
  126. fill: #fff;
  127. text-anchor: middle;
  128. font-weight: bold;
  129. }
  130. .grid-line {
  131. stroke: rgba(255, 255, 255, 0.1);
  132. stroke-width: 1;
  133. }
  134. .axis-label {
  135. font-size: 0.8rem;
  136. fill: #a0c8ff;
  137. }
  138. .curve-high {
  139. stroke: #ff6b6b;
  140. stroke-width: 3;
  141. fill: none;
  142. }
  143. .curve-low {
  144. stroke: #4facfe;
  145. stroke-width: 3;
  146. fill: none;
  147. }
  148. .curve-area {
  149. fill: url(#gradient);
  150. opacity: 0.3;
  151. }
  152. .chart-title {
  153. font-size: 1.2rem;
  154. fill: #a5b4fc;
  155. text-anchor: middle;
  156. }
  157. </style>
  158. </head>
  159. <body>
  160. <div id="app">
  161. <div class="container">
  162. <h1>SVG Path 平滑曲线实现</h1>
  163. <div class="controls">
  164. <button class="control-btn" :class="{active: curveType === 'linear'}" @click="curveType = 'linear'">线性曲线</button>
  165. <button class="control-btn" :class="{active: curveType === 'quadratic'}" @click="curveType = 'quadratic'">二次贝塞尔曲线</button>
  166. <button class="control-btn" :class="{active: curveType === 'cubic'}" @click="curveType = 'cubic'">三次贝塞尔曲线</button>
  167. <button class="control-btn" :class="{active: curveType === 'catmull'}" @click="curveType = 'catmull'">Catmull-Rom曲线</button>
  168. </div>
  169. <div class="chart-container">
  170. <svg class="chart" viewBox="0 0 800 400" preserveAspectRatio="xMidYMid meet">
  171. <!-- 定义渐变 -->
  172. <defs>
  173. <linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
  174. <stop offset="0%" stop-color="#ff6b6b" />
  175. <stop offset="100%" stop-color="#4facfe" />
  176. </linearGradient>
  177. </defs>
  178. <!-- 网格线 -->
  179. <g v-for="i in 5" :key="'grid-'+i">
  180. <line class="grid-line"
  181. :x1="0" :y1="i * 80"
  182. :x2="800" :y2="i * 80" />
  183. </g>
  184. <!-- X轴标签 -->
  185. <g v-for="(day, index) in forecast" :key="'label-'+index">
  186. <text class="axis-label"
  187. :x="getXPosition(index)"
  188. :y="390"
  189. text-anchor="middle">
  190. {{ day.day }}
  191. </text>
  192. </g>
  193. <!-- Y轴标签 -->
  194. <g v-for="i in 5" :key="'y-label-'+i">
  195. <text class="axis-label" x="20" :y="i * 80" text-anchor="end">
  196. {{ 40 - i * 5 }}℃
  197. </text>
  198. </g>
  199. <!-- 曲线区域填充 -->
  200. <path class="curve-area" :d="areaPath" />
  201. <!-- 高温曲线 -->
  202. <path class="curve-high" :d="highCurvePath" />
  203. <!-- 低温曲线 -->
  204. <path class="curve-low" :d="lowCurvePath" />
  205. <!-- 数据点 -->
  206. <g v-for="(day, index) in forecast" :key="'point-'+index">
  207. <circle class="point"
  208. :cx="getXPosition(index)"
  209. :cy="getTemperatureY(parseInt(day.highTemp))"
  210. r="5" />
  211. <circle class="point"
  212. :cx="getXPosition(index)"
  213. :cy="getTemperatureY(parseInt(day.lowTemp))"
  214. r="5" />
  215. <!-- 数值标签 -->
  216. <text class="point-label"
  217. :x="getXPosition(index)"
  218. :y="getTemperatureY(parseInt(day.highTemp)) - 15">
  219. {{ day.highTemp }}
  220. </text>
  221. <text class="point-label"
  222. :x="getXPosition(index)"
  223. :y="getTemperatureY(parseInt(day.lowTemp)) + 20">
  224. {{ day.lowTemp }}
  225. </text>
  226. </g>
  227. <!-- 图表标题 -->
  228. <text class="chart-title" x="400" y="30">7天温度变化曲线</text>
  229. </svg>
  230. </div>
  231. <div class="code-section">
  232. <h3 class="code-title">当前曲线代码</h3>
  233. <div class="code-block">
  234. <span class="comment">// {{ curveType }} 曲线路径</span><br>
  235. <span class="property">高温曲线</span>: <span class="value">{{ highCurvePath }}</span><br><br>
  236. <span class="property">低温曲线</span>: <span class="value">{{ lowCurvePath }}</span>
  237. </div>
  238. <div class="explanation">
  239. <h3>曲线类型说明</h3>
  240. <p><strong>线性曲线</strong>:使用直线连接各点,简单但不够平滑。</p>
  241. <p><strong>二次贝塞尔曲线</strong>:使用单个控制点创建平滑曲线。</p>
  242. <p><strong>三次贝塞尔曲线</strong>:使用两个控制点创建更平滑的曲线。</p>
  243. <p><strong>Catmull-Rom曲线</strong>:通过所有点的曲线,特别适合数据可视化。</p>
  244. </div>
  245. </div>
  246. </div>
  247. </div>
  248. <script>
  249. new Vue({
  250. el: '#app',
  251. data: {
  252. curveType: 'catmull',
  253. forecast: [
  254. { day: '周三', highTemp: '32', lowTemp: '21' },
  255. { day: '周四', highTemp: '28', lowTemp: '24' },
  256. { day: '周五', highTemp: '36', lowTemp: '22' },
  257. { day: '周六', highTemp: '30', lowTemp: '23' },
  258. { day: '周日', highTemp: '31', lowTemp: '25' },
  259. { day: '周一', highTemp: '29', lowTemp: '20' },
  260. { day: '周二', highTemp: '27', lowTemp: '19' }
  261. ]
  262. },
  263. computed: {
  264. highCurvePath() {
  265. return this.generateCurvePath(this.forecast.map(day => parseInt(day.highTemp)));
  266. },
  267. lowCurvePath() {
  268. return this.generateCurvePath(this.forecast.map(day => parseInt(day.lowTemp)));
  269. },
  270. areaPath() {
  271. const highPoints = this.forecast.map((day, i) => ({
  272. x: this.getXPosition(i),
  273. y: this.getTemperatureY(parseInt(day.highTemp))
  274. }));
  275. const lowPoints = this.forecast.map((day, i) => ({
  276. x: this.getXPosition(i),
  277. y: this.getTemperatureY(parseInt(day.lowTemp))
  278. })).reverse();
  279. let path = `M ${highPoints[0].x} ${highPoints[0].y}`;
  280. // 生成高温曲线
  281. path += this.generateCurveSegment(highPoints);
  282. // 连接到低温曲线终点
  283. path += ` L ${lowPoints[0].x} ${lowPoints[0].y}`;
  284. // 生成低温曲线(反向)
  285. path += this.generateCurveSegment(lowPoints);
  286. // 闭合路径
  287. path += ' Z';
  288. return path;
  289. }
  290. },
  291. methods: {
  292. getXPosition(index) {
  293. return 100 + index * (600 / (this.forecast.length - 1));
  294. },
  295. getTemperatureY(temperature) {
  296. // 温度范围:15-40℃,映射到图表高度
  297. const minTemp = 15;
  298. const maxTemp = 40;
  299. const normalized = (temperature - minTemp) / (maxTemp - minTemp);
  300. return 350 - (normalized * 300);
  301. },
  302. generateCurvePath(values) {
  303. const points = values.map((value, i) => ({
  304. x: this.getXPosition(i),
  305. y: this.getTemperatureY(value)
  306. }));
  307. let path = `M ${points[0].x} ${points[0].y}`;
  308. path += this.generateCurveSegment(points);
  309. return path;
  310. },
  311. generateCurveSegment(points) {
  312. if (points.length < 2) return '';
  313. let path = '';
  314. switch (this.curveType) {
  315. case 'linear':
  316. // 线性曲线 - 直接连接各点
  317. for (let i = 1; i < points.length; i++) {
  318. path += ` L ${points[i].x} ${points[i].y}`;
  319. }
  320. break;
  321. case 'quadratic':
  322. // 二次贝塞尔曲线
  323. for (let i = 1; i < points.length; i++) {
  324. const prev = points[i-1];
  325. const curr = points[i];
  326. // 控制点为两点中点
  327. const controlX = (prev.x + curr.x) / 2;
  328. const controlY = (prev.y + curr.y) / 2;
  329. if (i === 1) {
  330. path += ` Q ${controlX} ${controlY} ${curr.x} ${curr.y}`;
  331. } else {
  332. path += ` T ${curr.x} ${curr.y}`;
  333. }
  334. }
  335. break;
  336. case 'cubic':
  337. // 三次贝塞尔曲线
  338. for (let i = 1; i < points.length; i++) {
  339. const prev = points[i-1];
  340. const curr = points[i];
  341. // 控制点1为前一点向右偏移
  342. const control1X = prev.x + (curr.x - prev.x) * 0.3;
  343. const control1Y = prev.y;
  344. // 控制点2为当前点向左偏移
  345. const control2X = curr.x - (curr.x - prev.x) * 0.3;
  346. const control2Y = curr.y;
  347. path += ` C ${control1X} ${control1Y}, ${control2X} ${control2Y}, ${curr.x} ${curr.y}`;
  348. }
  349. break;
  350. case 'catmull':
  351. default:
  352. // Catmull-Rom样条曲线
  353. for (let i = 1; i < points.length; i++) {
  354. const p0 = i > 1 ? points[i-2] : points[i-1];
  355. const p1 = points[i-1];
  356. const p2 = points[i];
  357. const p3 = i < points.length - 1 ? points[i+1] : points[i];
  358. // 计算控制点
  359. const control1X = p1.x + (p2.x - p0.x) / 6;
  360. const control1Y = p1.y + (p2.y - p0.y) / 6;
  361. const control2X = p2.x - (p3.x - p1.x) / 6;
  362. const control2Y = p2.y - (p3.y - p1.y) / 6;
  363. path += ` C ${control1X} ${control1Y}, ${control2X} ${control2Y}, ${p2.x} ${p2.y}`;
  364. }
  365. break;
  366. }
  367. return path;
  368. }
  369. }
  370. });
  371. </script>
  372. </body>
  373. </html>