路线规划图
示例加载中…
import { Graph, Shape } from '@antv/x6'
import insertCss from 'insert-css'
Graph.registerConnector(
'loop-curve',
(start, end) => {
const sx = start.x
const sy = start.y
const ex = end.x
const ey = end.y
const mx = (sx + ex) / 2
const my = (sy + ey) / 2
const dir = sy < ey ? 1 : -1
const offset = 30
const path = [
`M ${sx} ${sy}`,
`C ${sx + 0} ${sy - 0 * dir}, ${mx - 50} ${my + 15 * dir}, ${mx - 20} ${my}`,
`C ${mx + 0} ${my - 6 * dir}, ${mx} ${my - offset * dir}, ${mx - 20} ${my - offset * dir + 10 * dir}`,
`C ${mx - 25} ${my - offset * dir + 15 * dir}, ${mx - 30} ${my - 10 * dir}, ${mx} ${my}`,
`C ${mx + 60} ${my + 20 * dir}, ${ex - 0} ${ey + 0 * dir}, ${ex} ${ey}`,
].join(' ')
return path
},
true,
)
Graph.registerEdge(
'plane-edge',
{
markup: [
{
tagName: 'svg',
selector: 'plane',
attrs: {
viewBox: '0 0 1024 1024',
width: 32,
height: 32,
y: -16,
opacity: 0,
},
children: [
{
tagName: 'path',
attrs: {
d: 'M488.5 391.1l-90.5-69.9a9.6 9.6 0 0 1 5.9-17.1h94.3a9.6 9.6 0 0 1 5.9 2l110.2 85.1H488.5z',
fill: '#fff',
},
},
{
tagName: 'path',
attrs: {
d: 'M488.7 294.4H374.6a19.2 19.2 0 0 0-15.2 7.5c-6.5 8.4-4.9 20.4 3.5 26.9l93 71.9h176.9l-114.4-85a19.2 19.2 0 0 0-11.7-4z m-26.2 87.1L374.6 313.6h114.1l87.8 67.9H462.4z',
fill: '#30699A',
},
},
{
tagName: 'path',
attrs: {
d: 'M143.5 344.4l-26.9 10a28.8 28.8 0 0 0-18.8 27v78.5a57.6 57.6 0 0 0 22.6 45.7l150.3 115.2a57.6 57.6 0 0 0 35 11.9h557c31.8 0 57.6-25.8 57.6-57.6a57.6 57.6 0 0 0-21.6-45l-163.4-130.6l-.5-.4a57.6 57.6 0 0 0-35.4-12.2l-480.6 0-47-38a28.8 28.8 0 0 0-28.1-4.6z m6.7 18a9.6 9.6 0 0 1 9.1 1.3l.3.2l52.3 42.2h487.4a38.4 38.4 0 0 1 23.5 8l.4.4l163.4 130.6a38.4 38.4 0 0 1 14.4 30c0 21-16.8 38.1-37.8 38.4l-.6 0H305.7a38.4 38.4 0 0 1-22.9-7.6l-.5-.3L132.1 460.5a38.4 38.4 0 0 1-14.6-29.7v-79a9.6 9.6 0 0 1 6-8.9l.3-.1l26.9-10z',
fill: '#30699A',
},
},
{
tagName: 'path',
attrs: {
d: 'M150.2 362.4a9.6 9.6 0 0 1 9.4 1.5l52.3 42.2h487.4a38.4 38.4 0 0 1 24 8.4l163.4 130.6a38.4 38.4 0 0 1 14.4 30c0 21.2-17.2 38.4-38.4 38.4H309L151.7 489.7a38.4 38.4 0 0 1-14.6-29.6v-.5v-87.2c0-2.1.7-4.1 1.9-5.8z',
fill: '#3982D1',
},
},
{
tagName: 'path',
attrs: {
d: 'M560.6 584.6L426.5 702.1c-4.5 4-4.7 10.6-.4 14.7 2.1 2.1 5.1 3.2 8.2 3.2h68.4c2.9 0 5.7-1 7.9-2.9h10l141.4-132.6',
fill: '#fff',
},
},
{
tagName: 'path',
attrs: {
d: 'M528 577.4L394.7 694.8c-8.7 7.7-9 20.8-.7 28.9 3.8 3.7 9 5.8 14.4 5.9h107.7c5.1 0 10-1.8 13.8-5l.4-.3l150.5-132.6l-12.7-14.4-150.5 132.6c-.3.3-.8.4-1.3.5h-107.7c-.6 0-1.2-.2-1.5-.5-.2-.2-.3-.3 0-.5l.1-.1l133.4-117.5l-12.7-14.4z',
fill: '#30699A',
},
},
{
tagName: 'path',
attrs: {
d: 'M805.4 492.7v19.2H700.2v-19.2z',
fill: '#fff',
},
},
{
tagName: 'path',
attrs: {
d: 'M431.5 497.5a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm56.8 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm57.4 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16zm57.4 0a8 8 0 1 1 0 16 8 8 0 0 1 0-16z',
fill: '#FFF',
},
},
],
},
// @ts-expect-error
...Shape.Edge.getMarkup(),
],
attrs: {
line: {
strokeDasharray: 4,
stroke: '#B8A58A',
},
plane: {
atConnectionRatio: 0,
},
},
},
true,
)
Shape.HTML.register({
shape: 'destination-node',
width: 160,
height: 80,
effect: ['data'],
html(cell) {
const {
img,
tag,
tagPosition = 'top-left',
des,
star,
} = cell.getData() ?? {}
const div = document.createElement('div')
div.className = 'routePlanning'
div.innerHTML = `
<div class="destination-node">
<div class="node-image-wrapper">
<div class="node-image-shadow"></div>
<img class="node-image" src='${img}' />
</div>
<div class="node-label-wrapper">
<div class="node-label">${des}</div>
<div class="node-label-shadow"></div>
<div class="node-label-tag node-label-tag-${tagPosition}">${tag}</div>
<svg width="30" height="30" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg" class="node-label-star" style="display: ${star ? 'block' : 'none'}" transform="rotate(0 0 0)">
<polygon
points="15,5 18.54,13.62 27,14.46 21,21.8 22.38,30 15,25.2 7.62,30 9,21.8 3,14.46 11.46,13.62"
fill="gold"
stroke="orange"
stroke-width="1"
/>
</svg>
</div>
</div>
`
return div
},
})
let currentPlaneIdx = 0
const graph = new Graph({
container: document.getElementById('container'),
grid: {
visible: true,
type: 'doubleMesh',
size: 25,
args: {
color: '#EEAD8A',
},
},
background: {
color: '#FAEFB3',
},
})
const stops = [
{
img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8HpYRKt7uzwAAAAAVmAAAAgAemJ7AQ/fmt.webp',
y: 225,
des: '法国 - 巴黎铁塔',
},
{
img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZFxTS428nZkAAAAAaLAAAAgAemJ7AQ/fmt.webp',
y: 150,
des: '美国 - 自由女神像',
tagPosition: 'bottom-left',
},
{
img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tfejTKFkjUUAAAAAW0AAAAgAemJ7AQ/fmt.webp',
y: 225,
des: '罗马 - 古罗马斗兽场',
tagPosition: 'top-right',
},
{
img: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*OM9JTbpmU-cAAAAAZbAAAAgAemJ7AQ/fmt.webp',
y: 175,
des: '英国 - 伦敦塔桥',
star: true,
},
]
stops.forEach((stop, index) => {
graph.addNode({
id: `stop${index + 1}`,
shape: 'destination-node',
x: 160 + index * 310,
y: stop.y,
data: { ...stop, tag: `${index + 1} stop` },
})
if (index + 1 === stops.length) return
graph.addEdge({
source: { cell: `stop${index + 1}` },
target: { cell: `stop${index + 2}` },
connector: { name: 'loop-curve' },
shape: 'plane-edge',
animation: [
[
{ 'attrs/line/strokeDashoffset': [0, -16] },
{
duration: 1000,
iterations: Infinity,
},
],
],
})
})
const fly = () => {
const edges = graph.getEdges()
const edge = edges[currentPlaneIdx]
edge.setAttrByPath('plane/opacity', 1)
const flyAni = edge.getAnimations().find((item) => item.id === 'fly')
if (flyAni) {
flyAni.play()
return
}
const ani = edge.animate(
{ 'attrs/plane/atConnectionRatio': 1 },
{ duration: 2000, id: 'fly' },
)
ani.onfinish = () => {
edge.setAttrByPath('plane/opacity', 0)
currentPlaneIdx =
currentPlaneIdx + 1 === edges.length ? 0 : currentPlaneIdx + 1
fly()
}
}
fly()
graph.centerContent()
graph.zoomToFit()
insertCss(`
.destination-node {
display: flex;
flex-direction: column;
align-items: center;
font-family:
"Hiragino Maru Gothic Pro",
/* 圆体最可爱,优先日文/繁体环境 */
"PingFang SC",
/* 苹方,简体中文首选,清新现代 */
"Hiragino Sans GB",
/* 冬青黑体简体,柔和清晰 */
"Helvetica Neue",
"Arial",
sans-serif;
}
.node-image-wrapper {
position: relative;
width: 110px;
height: 110px;
background: #fff;
border-radius: 50%;
}
.node-image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.node-image-shadow {
position: absolute;
top: 0;
left: 4px;
width: 100%;
height: 100%;
background: #EBB27D;
z-index: -1;
border-radius: 50%;
}
.node-label-wrapper {
margin-top: 20px;
background-color: #FEFCF2;
min-height: 120px;
border-radius: 4px;
width: 150px;
position: relative;
color: #838181;
box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.2);
transition: box-shadow 0.3s ease;
}
.node-label-wrapper:hover {
box-shadow: 8px 8px 20px rgba(0, 0, 0, 0.3);
}
.node-label-shadow {
position: absolute;
top: 0;
left: 3px;
width: 100%;
height: 100%;
background: #F7D0AE;
z-index: -1;
border-radius: 4px;
transform: rotate(-2deg);
}
.node-label {
padding: 8px;
z-index: 2;
}
.node-label-tag {
background: #EEAD8A;
color: #fff;
border-radius: 4px;
position: absolute;
line-height: 22px;
width: 60px;
text-align: center;
font-size: 12px;
font-weight: 800;
}
@keyframes shake {
from {
transform: rotate(0deg);
}
to {
transform: rotate(20deg);
}
}
.node-label-star {
position: absolute;
right: 8px;
bottom: 8px;
animation: shake 0.5s ease infinite;
animation-direction: alternate;
}
.node-label-tag-top-left {
left: 0;
top: 0;
transform: translateX(-50%) translateY(-50%) rotate(-20deg);
}
.node-label-tag-bottom-left {
left: 0;
bottom: 0;
transform: translateX(-50%) translateY(50%) rotate(20deg);
}
.node-label-tag-top-right {
right: 0;
top: 0;
transform: translateX(50%) translateY(-50%) rotate(20deg);
}
.node-label-tag-bottom-right {
right: 0;
bottom: 0;
transform: translateX(50%) translateY(50%) rotate(-20deg);
}
`)