canvas平时用的不多,但是有些开发场景还真得用它才能实现需求,这不,前段时间有一个这样的足迹图的需求,以下是实现的代码:
{ { historyItem.chatTitle }}
import { onBeforeMount, ref, watch } from 'vue'
import NavHeader from '@/layouts/Header.vue';
// import { dialogHistory } from '@/api/psychology.js'
import dayjs from 'dayjs'
console.log(uni)
const loading = ref(false)
const history = ref([])
const width = ref(0)
const height = ref(0)
const contextArr = ref([])
const getContentWidth = (inx) => {
const w = width.value.replace('px','') * 0.588
if(inx % 2 === 0) {
return {
top: contextArr.value[inx].y - 5 + 'px',
left: contextArr.value[inx].x + width.value.replace('px', '') * 0.74 - w + 'px',
width: w + 'px',
}
} else {
return {
top: contextArr.value[inx].y - 5 + 'px',
left: contextArr.value[inx].x - width.value.replace('px', '') * 0.74 + 'px',
width: w + 'px',
}
}
}
const getTimeWidth = (inx) => {
if(inx % 2 === 0) {
return {
top: contextArr.value[inx].y - 30 + 'px',
left: contextArr.value[inx].x + width.value.replace('px', '') * 0.74 - 102 + 'px'
}
} else {
return {
top: contextArr.value[inx].y - 30 + 'px',
left: contextArr.value[inx].x - width.value.replace('px', '') * 0.74 + 'px'
}
}
}
const getPlaceWidth = (inx) => {
if(inx % 2 === 0) {
return {
top: inx === contextArr.value.length - 1 ? contextArr.value[inx].y - 15 + 'px' : contextArr.value[inx].y - 10 + 'px',
left: inx === contextArr.value.length - 1 ? contextArr.value[inx].x - 7 + 'px' : contextArr.value[inx].x - 12 + 'px'
}
} else {
return {
top: inx === contextArr.value.length - 1 ? contextArr.value[inx].y - 15 + 'px' : contextArr.value[inx].y - 10 + 'px',
left: inx === contextArr.value.length - 1 ? contextArr.value[inx].x - 21 + 'px' : contextArr.value[inx].x - 18 + 'px'
}
}
}
const drawBgItem = (points, ctx, arr, path, offsetX = 0, offsetY = 0, imgW, imgH) => {
ctx.beginPath()
arr.forEach(e => {
ctx.drawImage(path, e.x + offsetX, e.y + offsetY, imgW, imgH)
})
if(arr.length > 1) {
const gap = arr[arr.length - 1].y - arr[arr.length - 2].y
if(points[points.length - 1].y >= arr[arr.length - 1].y + gap) {
ctx.drawImage(new URL(path, import.meta.url).href, arr[arr.length -1].x + offsetX, arr[arr.length - 1].y + gap + offsetY, imgW, imgH)
}
}
}
const draw = (ctx, canvas) => {
console.log(ctx, canvas)
contextArr.value = []
const canvasWidth = canvas.width * 0.2
const h = history.value.length * 190 + 22 < height.value.replace('px', '') ? height.value.replace('px', '') : history.value.length * 190 + 22
const amplitude = 276
console.log('?h', h)
const frequency = Math.PI / 122
const points = []
let bluePaths = []
let max = 0
let basePointY = 178
for(let i = basePointY; i <= h; i ++) {
const x = canvasWidth / 100 + Math.sin(i * frequency) * amplitude * 0.56 + canvasWidth * 2.5
const y = i
if(Math.sin(i * frequency) === 1 || Math.sin(i * frequency) === -1 ) {
console.log('?i', i, h)
contextArr.value.push({
inx: contextArr.value.length,
points,
x,
y
})
if(contextArr.value.length >= history.value.length) {
if(Math.sin(i * frequency) === 1) {
points.push({
x: contextArr.value[contextArr.value.length - 1].x - width.value.replace('px', '') * 1,
y: contextArr.value[contextArr.value.length - 1].y + 240
})
} else {
points.push({
x: contextArr.value[contextArr.value.length - 1].x + width.value.replace('px', '') * 1,
y: contextArr.value[contextArr.value.length - 1].y + 240
})
}
break
}
max = i
}
points.push({x, y})
if(max === i && i + 300 >= h) {
break
}
}
bluePaths = [...points]
/**左边箭头1 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i % 4 === 0), new URL('./assets/images/timeline-left-1.png', import.meta.url).href, -45, 10, 53.5, 55.5)
/* end 左边箭头 */
/**左边脚印 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i % 2 === 0 && i !== 0), new URL('./assets/images/timeline-left-2.png', import.meta.url).href, -46, -145, 80, 135)
/**end 左边脚印 */
/**左边箭头2 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i === 2 || i % 4 !== 0 && (i % 4) % 2 === 0), new URL('./assets/images/timeline-left-3.png', import.meta.url).href, -40, 30, 50, 50)
/**end 左边箭头2 */
/**右边箭头1 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i % 4 === 1), new URL('./assets/images/timeline-right-1.png', import.meta.url).href, -10, 35, 47.5, 51.5)
/**end 右边箭头1 */
/**右边脚印 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i % 2 !== 0), new URL('./assets/images/timeline-right-2.png', import.meta.url).href, -35, -145, 80, 135)
/**end 右边脚印 */
/**右边箭头2 */
drawBgItem(points, ctx, contextArr.value.filter((_, i) => i % 3 === 0 && i !== 0), new URL('./assets/images/timeline-right-3.png', import.meta.url).href, -60, 30, 77, 68)
/**右边箭头2 */
/**紫色 */
const offsetX = 3
const offsetY = 10
const circleOffsetX = 0
const circleOffsetY = 3
ctx.beginPath()
ctx.arc(canvasWidth / 100 + Math.sin(basePointY * frequency) * amplitude * 0.56 + canvasWidth * 2.5 + offsetX + circleOffsetX, basePointY + offsetY + circleOffsetY, 16, 0, 2 * Math.PI)
ctx.setFillStyle('#cbc6ff')
ctx.fill()
ctx.beginPath()
ctx.setStrokeStyle('#cbc6ff')
ctx.setLineWidth(32)
for(let i = 1; i < bluePaths.length; i++) {
const cpx = (bluePaths[i].x + bluePaths[i - 1].x + 2 * offsetX) / 2
const cpy = (bluePaths[i].y + bluePaths[i - 1].y + 2 * offsetY) / 2
ctx.bezierCurveTo(bluePaths[i - 1].x + offsetX, bluePaths[i - 1].y + offsetY, cpx, cpy, bluePaths[i].x + offsetX, bluePaths[i].y + offsetY)
}
ctx.stroke()
/* end 紫色 */
/**蓝色 */
ctx.beginPath()
ctx.arc(canvasWidth / 100 + Math.sin(basePointY * frequency) * amplitude * 0.56 + canvasWidth * 2.5 + circleOffsetX, basePointY + circleOffsetY, 20, 0, 2 * Math.PI)
ctx.setFillStyle('#c9ddff')
ctx.fill()
ctx.beginPath()
ctx.setStrokeStyle('#c9ddff'),
ctx.setLineWidth(37)
for(let i = 1; i < bluePaths.length; i++) {
const cpx = (bluePaths[i].x + bluePaths[i - 1].x) / 2
const cpy = (bluePaths[i].y + bluePaths[i - 1].y) / 2
ctx.bezierCurveTo(bluePaths[i - 1].x, bluePaths[i - 1].y, cpx, cpy, bluePaths[i].x, bluePaths[i].y)
}
ctx.stroke()
/** end 蓝色*/
/** 白色 */
ctx.setStrokeStyle('#fff')
ctx.setLineWidth(2)
ctx.setLineDash([30, 30])
ctx.beginPath()
points.forEach(point => {
ctx.lineTo(point.x, point.y)
})
ctx.stroke()
/** end 白色*/
ctx.draw()
}
const handleClick = e => {
const rect = e.instance.getBoundingClientRect()
console.log('e', e,'hasInGraph', hasInGraph({x: e.detail.x, y: e.detail.y - rect.top} ), 'rect', rect)
}
const hasInGraph = (point) => {
for(let e of contextArr.value) {
const inx = hasInSingleGraph(e, point)
if(inx !== -1) {
return inx
}
}
return -1
}
const hasInSingleGraph = (obj, point) => {
const { points : polygon } = obj
const { x , y } = point
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[j].y;
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
return obj.inx
}
}
return -1
}
const initCon = async() => {
loading.value = false
const info = await uni.getSystemInfo()
const r = [
{"id":"11","report":"4","time":"2024-10-25T08:42:53.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"22","report":"3","time":"2024-10-25T08:30:43.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"33","report":"2","time":"2024-10-25T08:25:12.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"44","report":"","time":"2024-10-25T07:51:00.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"55","report":"2","time":"2024-10-25T07:28:55.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"66","report":"1","time":"2024-10-25T07:28:55.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"77","report":"1","time":"2024-10-25T07:28:55.000+00:00", chatTitle: '这是一段文本,用于测试'},
{"id":"88","report":"1","time":"2024-10-25T07:15:34.000+00:00",chatTitle: '这是一段文本,用于测试'},
{"id":"99","report":"1","time":"2024-10-25T07:12:39.000+00:00",chatTitle: '这是一段文本,用于测试'}
].filter(e => e.report).map(e => ({
...e,
time: dayjs(e.time).format('YYYY-MM-DD'),
oriTime: dayjs(e.time).format('YYYY-MM-DD HH:mm:ss')
}))
history.value = r
setTimeout(()=>{
width.value = info.windowWidth + 'px'
height.value = history.value.length * 150 + 44 < info.windowHeight ? info.windowHeight + 'px' : history.value.length * 150 + 44 + 'px'
},1000)
}
watch(() => height.value, val => {
if(val) {
setTimeout(() => {
console.log('?height', val)
draw(uni.createCanvasContext('cus-canvas'), {
width: +width.value.replace('px', ''),
height: +height.value.replace('px', '')
})
loading.value = false
}, 100)
}
})
onBeforeMount(async() => {
initCon()
})
.conversation-history {
.history-header {
position: fixed;
width: calc(100vw - 48rpx);
height: 46rpx;
background-color: #fff;
top: 0;
left: 0;
z-index: 9;
}
.con {
position: relative;
padding-top: 46rpx;
height: calc(110vh );
background: url('./assets/images/timeline-bg.png') repeat center/100% 100%;
top: -20px;
}
.mark {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
.timeStop {
font-size: 12px;
.content {
color: #595b63;
font-size: 27rpx;
position: absolute;
height: 75rpx;
&.right {
text-align: right;
}
&.left {
text-align: left;
}
}
.time {
color: #439bff;
font-family: DingTalk JinBuTi, PingFangSC-Medium, PingFang SC; ;
font-style: italic;
font-size: 38rpx;
position: absolute;
}
.plane {
position: absolute;
.plane-icon {
width: 62rpx;
height: 62rpx;
}
}
.sign {
position: absolute;
top: 80rpx;
left: 67.3rpx;
width: 205rpx;
height: 134rpx;
}
}
}
}