| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SVG Path 平滑曲线实现</title>
- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- body {
- background: linear-gradient(135deg, #1a2a6c, #0d1b36);
- min-height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 20px;
- color: #fff;
- }
- .container {
- width: 100%;
- max-width: 900px;
- background: rgba(13, 27, 54, 0.8);
- border-radius: 20px;
- padding: 30px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
- border: 1px solid rgba(76, 201, 240, 0.3);
- }
- h1 {
- text-align: center;
- color: #4cc9f0;
- margin-bottom: 30px;
- text-shadow: 0 0 10px rgba(76, 201, 240, 0.5);
- }
- .chart-container {
- width: 100%;
- height: 400px;
- background: rgba(30, 45, 80, 0.5);
- border-radius: 10px;
- padding: 20px;
- margin-bottom: 30px;
- position: relative;
- }
- .chart {
- width: 100%;
- height: 100%;
- }
- .controls {
- display: flex;
- justify-content: center;
- gap: 15px;
- margin-bottom: 20px;
- }
- .control-btn {
- background: rgba(76, 201, 240, 0.3);
- border: none;
- padding: 10px 20px;
- border-radius: 5px;
- color: white;
- font-weight: bold;
- cursor: pointer;
- transition: all 0.3s;
- }
- .control-btn:hover {
- background: rgba(76, 201, 240, 0.5);
- }
- .control-btn.active {
- background: rgba(76, 201, 240, 0.7);
- box-shadow: 0 0 10px rgba(76, 201, 240, 0.5);
- }
- .code-section {
- background: rgba(0, 0, 0, 0.3);
- border-radius: 10px;
- padding: 20px;
- margin-top: 30px;
- }
- .code-title {
- color: #a5b4fc;
- margin-bottom: 15px;
- }
- .code-block {
- background: #1e1e1e;
- border-radius: 8px;
- padding: 15px;
- font-family: 'Courier New', monospace;
- overflow-x: auto;
- line-height: 1.5;
- }
- .comment {
- color: #6a9955;
- }
- .property {
- color: #9cdcfe;
- }
- .value {
- color: #ce9178;
- }
- .explanation {
- background: rgba(30, 30, 60, 0.5);
- padding: 15px;
- border-radius: 8px;
- margin-top: 20px;
- border-left: 4px solid #4cc9f0;
- }
- .explanation h3 {
- color: #a5b4fc;
- margin-bottom: 10px;
- }
- .point {
- fill: #4cc9f0;
- stroke: #fff;
- stroke-width: 2;
- cursor: pointer;
- transition: all 0.3s;
- }
- .point:hover {
- r: 8;
- fill: #ff6b6b;
- }
- .point-label {
- font-size: 0.8rem;
- fill: #fff;
- text-anchor: middle;
- font-weight: bold;
- }
- .grid-line {
- stroke: rgba(255, 255, 255, 0.1);
- stroke-width: 1;
- }
- .axis-label {
- font-size: 0.8rem;
- fill: #a0c8ff;
- }
- .curve-high {
- stroke: #ff6b6b;
- stroke-width: 3;
- fill: none;
- }
- .curve-low {
- stroke: #4facfe;
- stroke-width: 3;
- fill: none;
- }
- .curve-area {
- fill: url(#gradient);
- opacity: 0.3;
- }
- .chart-title {
- font-size: 1.2rem;
- fill: #a5b4fc;
- text-anchor: middle;
- }
- </style>
- </head>
- <body>
- <div id="app">
- <div class="container">
- <h1>SVG Path 平滑曲线实现</h1>
- <div class="controls">
- <button class="control-btn" :class="{active: curveType === 'linear'}" @click="curveType = 'linear'">线性曲线</button>
- <button class="control-btn" :class="{active: curveType === 'quadratic'}" @click="curveType = 'quadratic'">二次贝塞尔曲线</button>
- <button class="control-btn" :class="{active: curveType === 'cubic'}" @click="curveType = 'cubic'">三次贝塞尔曲线</button>
- <button class="control-btn" :class="{active: curveType === 'catmull'}" @click="curveType = 'catmull'">Catmull-Rom曲线</button>
- </div>
- <div class="chart-container">
- <svg class="chart" viewBox="0 0 800 400" preserveAspectRatio="xMidYMid meet">
- <!-- 定义渐变 -->
- <defs>
- <linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="#ff6b6b" />
- <stop offset="100%" stop-color="#4facfe" />
- </linearGradient>
- </defs>
- <!-- 网格线 -->
- <g v-for="i in 5" :key="'grid-'+i">
- <line class="grid-line"
- :x1="0" :y1="i * 80"
- :x2="800" :y2="i * 80" />
- </g>
- <!-- X轴标签 -->
- <g v-for="(day, index) in forecast" :key="'label-'+index">
- <text class="axis-label"
- :x="getXPosition(index)"
- :y="390"
- text-anchor="middle">
- {{ day.day }}
- </text>
- </g>
- <!-- Y轴标签 -->
- <g v-for="i in 5" :key="'y-label-'+i">
- <text class="axis-label" x="20" :y="i * 80" text-anchor="end">
- {{ 40 - i * 5 }}℃
- </text>
- </g>
- <!-- 曲线区域填充 -->
- <path class="curve-area" :d="areaPath" />
- <!-- 高温曲线 -->
- <path class="curve-high" :d="highCurvePath" />
- <!-- 低温曲线 -->
- <path class="curve-low" :d="lowCurvePath" />
- <!-- 数据点 -->
- <g v-for="(day, index) in forecast" :key="'point-'+index">
- <circle class="point"
- :cx="getXPosition(index)"
- :cy="getTemperatureY(parseInt(day.highTemp))"
- r="5" />
- <circle class="point"
- :cx="getXPosition(index)"
- :cy="getTemperatureY(parseInt(day.lowTemp))"
- r="5" />
- <!-- 数值标签 -->
- <text class="point-label"
- :x="getXPosition(index)"
- :y="getTemperatureY(parseInt(day.highTemp)) - 15">
- {{ day.highTemp }}
- </text>
- <text class="point-label"
- :x="getXPosition(index)"
- :y="getTemperatureY(parseInt(day.lowTemp)) + 20">
- {{ day.lowTemp }}
- </text>
- </g>
- <!-- 图表标题 -->
- <text class="chart-title" x="400" y="30">7天温度变化曲线</text>
- </svg>
- </div>
- <div class="code-section">
- <h3 class="code-title">当前曲线代码</h3>
- <div class="code-block">
- <span class="comment">// {{ curveType }} 曲线路径</span><br>
- <span class="property">高温曲线</span>: <span class="value">{{ highCurvePath }}</span><br><br>
- <span class="property">低温曲线</span>: <span class="value">{{ lowCurvePath }}</span>
- </div>
- <div class="explanation">
- <h3>曲线类型说明</h3>
- <p><strong>线性曲线</strong>:使用直线连接各点,简单但不够平滑。</p>
- <p><strong>二次贝塞尔曲线</strong>:使用单个控制点创建平滑曲线。</p>
- <p><strong>三次贝塞尔曲线</strong>:使用两个控制点创建更平滑的曲线。</p>
- <p><strong>Catmull-Rom曲线</strong>:通过所有点的曲线,特别适合数据可视化。</p>
- </div>
- </div>
- </div>
- </div>
- <script>
- new Vue({
- el: '#app',
- data: {
- curveType: 'catmull',
- forecast: [
- { day: '周三', highTemp: '32', lowTemp: '21' },
- { day: '周四', highTemp: '28', lowTemp: '24' },
- { day: '周五', highTemp: '36', lowTemp: '22' },
- { day: '周六', highTemp: '30', lowTemp: '23' },
- { day: '周日', highTemp: '31', lowTemp: '25' },
- { day: '周一', highTemp: '29', lowTemp: '20' },
- { day: '周二', highTemp: '27', lowTemp: '19' }
- ]
- },
- computed: {
- highCurvePath() {
- return this.generateCurvePath(this.forecast.map(day => parseInt(day.highTemp)));
- },
- lowCurvePath() {
- return this.generateCurvePath(this.forecast.map(day => parseInt(day.lowTemp)));
- },
- areaPath() {
- const highPoints = this.forecast.map((day, i) => ({
- x: this.getXPosition(i),
- y: this.getTemperatureY(parseInt(day.highTemp))
- }));
- const lowPoints = this.forecast.map((day, i) => ({
- x: this.getXPosition(i),
- y: this.getTemperatureY(parseInt(day.lowTemp))
- })).reverse();
- let path = `M ${highPoints[0].x} ${highPoints[0].y}`;
- // 生成高温曲线
- path += this.generateCurveSegment(highPoints);
- // 连接到低温曲线终点
- path += ` L ${lowPoints[0].x} ${lowPoints[0].y}`;
- // 生成低温曲线(反向)
- path += this.generateCurveSegment(lowPoints);
- // 闭合路径
- path += ' Z';
- return path;
- }
- },
- methods: {
- getXPosition(index) {
- return 100 + index * (600 / (this.forecast.length - 1));
- },
- getTemperatureY(temperature) {
- // 温度范围:15-40℃,映射到图表高度
- const minTemp = 15;
- const maxTemp = 40;
- const normalized = (temperature - minTemp) / (maxTemp - minTemp);
- return 350 - (normalized * 300);
- },
- generateCurvePath(values) {
- const points = values.map((value, i) => ({
- x: this.getXPosition(i),
- y: this.getTemperatureY(value)
- }));
- let path = `M ${points[0].x} ${points[0].y}`;
- path += this.generateCurveSegment(points);
- return path;
- },
- generateCurveSegment(points) {
- if (points.length < 2) return '';
- let path = '';
- switch (this.curveType) {
- case 'linear':
- // 线性曲线 - 直接连接各点
- for (let i = 1; i < points.length; i++) {
- path += ` L ${points[i].x} ${points[i].y}`;
- }
- break;
- case 'quadratic':
- // 二次贝塞尔曲线
- for (let i = 1; i < points.length; i++) {
- const prev = points[i-1];
- const curr = points[i];
- // 控制点为两点中点
- const controlX = (prev.x + curr.x) / 2;
- const controlY = (prev.y + curr.y) / 2;
- if (i === 1) {
- path += ` Q ${controlX} ${controlY} ${curr.x} ${curr.y}`;
- } else {
- path += ` T ${curr.x} ${curr.y}`;
- }
- }
- break;
- case 'cubic':
- // 三次贝塞尔曲线
- for (let i = 1; i < points.length; i++) {
- const prev = points[i-1];
- const curr = points[i];
- // 控制点1为前一点向右偏移
- const control1X = prev.x + (curr.x - prev.x) * 0.3;
- const control1Y = prev.y;
- // 控制点2为当前点向左偏移
- const control2X = curr.x - (curr.x - prev.x) * 0.3;
- const control2Y = curr.y;
- path += ` C ${control1X} ${control1Y}, ${control2X} ${control2Y}, ${curr.x} ${curr.y}`;
- }
- break;
- case 'catmull':
- default:
- // Catmull-Rom样条曲线
- for (let i = 1; i < points.length; i++) {
- const p0 = i > 1 ? points[i-2] : points[i-1];
- const p1 = points[i-1];
- const p2 = points[i];
- const p3 = i < points.length - 1 ? points[i+1] : points[i];
- // 计算控制点
- const control1X = p1.x + (p2.x - p0.x) / 6;
- const control1Y = p1.y + (p2.y - p0.y) / 6;
- const control2X = p2.x - (p3.x - p1.x) / 6;
- const control2Y = p2.y - (p3.y - p1.y) / 6;
- path += ` C ${control1X} ${control1Y}, ${control2X} ${control2Y}, ${p2.x} ${p2.y}`;
- }
- break;
- }
- return path;
- }
- }
- });
- </script>
- </body>
- </html>
|