之前前面写过一遍有关canvas移动端适配的稿文,测试测出来还有点bug,绘制的图有点模糊,这里完善下。
为什么会导致这个问题,因为在高屏分辨率下1个像素等于4个像素比的原因导致
解决办法是,所有的数值,必须要 乘以 dpr,
假如,页面是1200*680 的高度,我的图片得是2倍的图2400*1360
canvas 设置成2400*1360的width ,这样页面会被放大,
最后再用css样式去控制canvas的宽高,就可以解决这个问题
类似于要达到下面这个效果
<canvas width="2400" height="1360" style="width: 1200px;height: 680px;"></canvas>
只不过,canvas的width属于与style里面的width属性意义不一样而已。
当然在实际h5页面里,宽高不会是这样写死的,都是动态获取。
例如,我这个设计是横屏,
canvas的width、height得是实际可视内容的宽度,得动态获取
const canvasFun = () => {
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
let dpr = window.devicePixelRatio; //------- 第一步
const image = new Image()
if (basicInfo.value.backgroundImage) {
const url = `${publicUrl.getFile}?fileName=${
basicInfo.value.backgroundImage
}&dispositionType=2&satoken=${Cookies.get('satoken')}`
image.src = url // 图片链接
// 监听图片加载完成
image.onload = function () {
// --------------------------动态获取屏幕可视宽高,作为canvas 的宽高
canvas.width = Math.round(window.innerHeight * dpr)
canvas.height = Math.round(window.innerWidth * dpr)
//-------------------------- 再用样式去控制实际样式宽高
canvas.style.width = window.innerHeight + 'px';
canvas.style.height = window.innerWidth + 'px';
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
}
}
}
后面如果有去动态写入样式,只需要将所有的值都 乘以dpr 这个固定变量
记得,如果有点位的展示,点位也需要 乘以dpr
下面附上完整的优化后的代码
<template>
<div class="interactiveActivitySecond" id="interactiveActivitySecond">
<div class="arrow-left" @click="goback">
</div>
<div class="intro" @click="showDialog"></div>
<canvas id="canvas" ref="canvas"></canvas>
<!-- 说明弹框 -->
<van-overlay :show="show" :z-index="2007" class-name="vanOverlays" @click="show = false" >
<introDialog :dialog-object="dialogConfimVisi" @dialogClose="dialogCofinmClose"
></introDialog>
</van-overlay>
</div>
</template>
<script lang="ts" setup>
import { toRefs, onBeforeUnmount,computed,reactive, watch, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import Cookies from 'js-cookie'
import publicUrl from '@/api/file-api'
import introDialog from './dialog.vue'
import { Toast } from 'vant'
const props = defineProps({
basicInfo: {
type: Object,
default: () => {},
},
})
const { basicInfo } = toRefs(props)
import { useSecondCard } from '@/hooks/useSecondCard'
import { useCardCommon } from '@/hooks/useCardCommon'
// 解构公共方法
const { goSecondPage, showBtnName, showSignBtnName } = useSecondCard()
const { goReport, show7Days, showReupload } = useCardCommon()
const router = useRouter()
const newArr=ref([])
const dialogConfimVisi = reactive({
dialogVisible: false,
intro:''
})
const show=ref(false)
//绘制圆角
const drawRoundedRect = (ctx, x, y, width, height, radius,shadow) => {
// 开始绘制路径
ctx.beginPath()
// 左上角圆角
ctx.moveTo(x + radius, y)
ctx.lineTo(x + width - radius, y)
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
// 右上角圆角
ctx.lineTo(x + width, y + height - radius)
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
// 右下角圆角
ctx.lineTo(x + radius, y + height)
ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
// 左下角圆角
ctx.lineTo(x, y + radius)
ctx.quadraticCurveTo(x, y, x + radius, y)
// 关闭路径
ctx.closePath()
if (shadow == 1) {
ctx.shadowBlur = 30 // 阴影模糊度
ctx.shadowColor = 'rgba(0,0,0,.2)' // 阴影颜色
} else {
ctx.shadowBlur = 0 // 阴影模糊度
ctx.shadowColor = '' // 阴影颜色
}
// 填充矩形
ctx.fill()
}
//根据按钮文字显示不同背景色
const showBtnBgc = (val) => {
if (val == '参与') {
return '#DAE8FF'
} else if (val == '再次作答') {
return '#DAE8FF'
} else if (val == '查看') {
return '#FBEDDF'
} else if (val == '已放弃') {
return '#F0F3F7'
} else if (val == '完成') {
return '#DDF3E0'
} else if (val == '过期') {
return '#F0F3F7'
}
}
//根据按钮文字设置不同字体颜色
const showBtnColor = (val) => {
if (val == '参与') {
return '#004ED9'
} else if (val == '再次作答') {
return '#004ED9'
} else if (val == '查看') {
return '#E1892A'
} else if (val == '已放弃') {
return '#9398A7'
} else if (val == '完成') {
return '#31BE44'
} else if (val == '过期') {
return '#9398A7'
}
}
// 根据宽高比比例计算实际点位位置
const pointsFun=(arr,dpr)=>{
let webWidth = 1200
let webHeight = 680
let screenWidth = window.innerHeight //屏幕宽
let screenHeight = window.innerWidth // 屏幕高
arr.forEach(item=>{
let webx = webWidth / item.pointX
let weby = webHeight / item.pointY
let pintX= screenWidth / webx
let pintY= screenHeight / weby
item.pintX=pintX * dpr
item.pintY=pintY * dpr
})
return arr
}
//假数据
const points = ref([
{
id: 1260195,
groupId: 1290194,
x: 530,
y: 470,
name: '班会课班会陆班会课会课陆课会课',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 1,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-30 00:00:00',
width: 128,
height: 77,
paperId: 1260195,
}, //班会课
{
id: 1290187,
groupId: 1290194,
x: 800,
y: 460,
name: '校门口',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 0,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-30 00:00:00',
width: 128,
height: 77,
paperId: 1290187,
}, //校门口
{
id: 1260194,
groupId: 1290194,
x: 220,
y: 440,
name: '卫生角',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 0,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-30 00:00:00',
width: 128,
height: 77,
paperId: 1260194
}, //卫生角
{
id: 1290186,
groupId: 1290194,
x: 300,
y: 230,
name: '黑板报',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 0,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-30 00:00:00',
width: 128,
height: 77,
paperId: 1290186
}, //黑板报
{
id: 1260196,
groupId: 1290194,
x: 685,
y: 165,
name: '办公室',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 0,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-12 00:00:00',
width: 128,
height: 77,
paperId: 1260196
}, //办公室
{
id: 1290188,
groupId: 1290194,
x: 920,
y: 370,
name: '传达室',
type: 0,
state: 0,
respondTimes: 2,
reuploadFlag: 0,
beginTime: '2024-05-30 00:00:00',
endTime: '2024-06-06 00:00:00',
width: 128,
height: 77,
paperId:1290188
}, //传达室
])
const insertCharAt = (str, char, index) => {
// 使用slice方法分割字符串,并在index位置插入char
return str.slice(0, index) + char + str.slice(index)
}
const getSubstringBefore = (str, index) => {
return str.substring(0, index)
}
//判断是否超过9个字符,超过了就省略号
const stringLength = (val) => {
let text
if (val && val.length < 7) {
// console.log('11111')
return val
} else if (val && val.length > 6 && val.length < 10) {
// console.log('22222')
//大于12个字符加换行符
const charToInsert = '\n'
const positionOth = 6
const newResult = insertCharAt(val, charToInsert, positionOth)
const text = newResult + '...'
return text
} else if (val && val.length > 6 && val.length > 10) {
// console.log('33333')
const originalString = val
// 在第20个字符那拼接省略号
const position = 10
const result = getSubstringBefore(originalString, position)
const newResult = result + '...'
// 在第12个字符那插入换行符
const charToInsert = '\n'
const positionOth = 6
text = insertCharAt(newResult, charToInsert, positionOth)
return text
}
}
//设置高度
const setCanvasDiaHeight = (val) => {
let height
if (val && val.length < 7) {
height = 78
return height
} else {
height = 96
return height
}
}
const setleft = (val,x,dpr) => {
let leftX
if (val && val.length == 2) {
// 参与、查看、完成、过期
leftX = x + 15 * dpr
}else if (val && val.length == 3) {
//已放弃、待解锁
leftX = x + 5 * dpr
}else if (val && val.length == 4) {
//再次参与
leftX = x - 5 * dpr
}
return leftX
}
const iconIndexFun=(val)=>{
if(val=='完成'){
return 2
}else if(val=='已放弃'){
return 1
}else if(val=='查看'){
return 0
}else if(val=='待解锁'){
return 3
}else if(val=='参与'||val=='再次作答'){
return 4
}else if(val=='过期'){
return 5
}else {
return 6 //代表没有
}
}
let getPixelRatio = function(context) {
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStore;
};
const iconIndex =ref(0)
const canvasFun = () => {
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
let dpr = window.devicePixelRatio; // 假设dpr为2
//图标数组
const pointImageURLs = [
require('@/assets/images/second-image/icon/read.png'),
require('@/assets/images/second-image/icon/giveUp.png'),
require('@/assets/images/second-image/icon/finish.png'),
require('@/assets/images/second-image/icon/lock.png'),
require('@/assets/images/second-image/icon/exclamationMark.png'),
require('@/assets/images/second-image/icon/overdue.png'),
];
const image = new Image()
if (basicInfo.value.backgroundImage) {
const url = `${publicUrl.getFile}?fileName=${
basicInfo.value.backgroundImage
}&dispositionType=2&satoken=${Cookies.get('satoken')}`
image.src = url // 图片链接
// 监听图片加载完成
image.onload = function () {
canvas.width = Math.round(window.innerHeight * dpr)
canvas.height = Math.round(window.innerWidth * dpr)
canvas.style.width = window.innerHeight + 'px';
canvas.style.height = window.innerWidth + 'px';
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
newArr.value=pointsFun(basicInfo.value.chooseSubjectList,dpr)
// 根据点位写入样式
newArr.value.forEach((point) => {
//--------------设置高度
let diaheight = setCanvasDiaHeight(point.name)
// 设置填充颜色为黄色
ctx.fillStyle = '#ffffff'
// 1-------------绘制矩形
let pointX = point.pintX - 50 * dpr //实际点位再偏移
let pointY = point.pintY - diaheight * dpr
//创建一个填充矩形(弹框)
drawRoundedRect(ctx, pointX , pointY , 128 * dpr, diaheight * dpr , 14 * dpr,1)
ctx.font = `${16 * dpr}px AlibabaPuHuiTi`// 设置字体大小和颜色
ctx.fontWeight = 'bold' // 再设置字重
ctx.fillStyle = '#181A1C' // 设置字体大小和颜色
ctx.textAlign = 'center' // 设置文本水平居中
ctx.textBaseline = 'middle' // 设置文本基线
// 处理标题名称,插入换行符
let pointName = stringLength(point.name)
// 在换行处拆分文本并多次调用 fillText() 来模拟换行
var lineheight = 18 * dpr
var lines = pointName.split('\n')
// 在弹框中间写入文本
for (var i = 0; i < lines.length; i++) {
if (i == 0) {
ctx.fillText(lines[i], pointX + 64 * dpr, pointY + 30 * dpr+ i * lineheight)
} else {
ctx.fillText(lines[i], pointX + 54 * dpr, pointY + 30 * dpr+ i * lineheight)
}
}
// 2-------------绘制倒三角
// 气泡框的起点坐标 (x, y)
var x = point.pintX
var y = point.pintY
// 倒三角的大小
var width = 24 * dpr
var height = 17 * dpr
// 开始绘制
ctx.beginPath()
// 绘制倒三角形
ctx.moveTo(x, y) // 气泡框左上角
ctx.lineTo(x + width, y) // 气泡框右上角
ctx.lineTo(x + width / 2, y + height) // 气泡框中间底部
ctx.lineTo(x, y) // 闭合路径
// 设置三角形颜色并填充
ctx.fillStyle = '#FFFFFF'
ctx.fill()
// // 3、------------------设置按钮-矩形的宽度和高度
var heights = 20 * dpr
//设置圆角的半径
var radius = 10 * dpr
ctx.beginPath()
let obj = showBtnName.value(point)
//-------------------是否有带重传标志,再来设置,矩形的宽度和高度
if (point.reuploadFlag == 1) {
// 2个按钮
drawRoundedRect(ctx, point.pintX - 35 * dpr, point.pintY - 30 * dpr, 43 * dpr, heights, radius,0)
} else {
// 1个按钮
drawRoundedRect(ctx, point.pintX - 30 * dpr, point.pintY - 30 * dpr, 96 * dpr, heights, radius,0)
}
let color = showBtnBgc(obj.label)
ctx.fillStyle = color
ctx.fill()
//-------------------设置文字
ctx.beginPath()
let objq = showBtnName.value(point)
let colors = showBtnColor(objq.label)
ctx.fillStyle = colors
ctx.font = `${12 * dpr}px AlibabaPuHuiTi` // 设置字体大小和颜色
if (point.reuploadFlag == 1) {
ctx.fillText(objq.label, point.pintX - 15 * dpr, point.pintY - 18 * dpr)
// 绘制带重传按钮
ctx.fillStyle = '#E0831F'
ctx.fillText('待重传', point.pintX + 40 * dpr, point.pintY - 18 * dpr)
ctx.closePath()
ctx.fill()
} else {
let leftX=setleft(objq.label,point.pintX,dpr)
ctx.fillText(objq.label, leftX, point.pintY - 18 * dpr)
ctx.textAlign = 'center' // 设置文本水平居中
ctx.textBaseline = 'middle' // 设置文本基线
ctx.closePath()
ctx.fill()
}
// // 根据状态去展示不同图标
iconIndex.value = iconIndexFun(obj.label)
let diaheighty = setCanvasDiaHeight(point.name)
let pointXs = point.pintX - (60 * dpr) //实际点位再偏移
let pointYs = point.pintY - (diaheighty * dpr) - 7 * dpr
if(iconIndex.value==6||iconIndex.value==4){
let show=show7Days.value(point)
if(show){
//展示7天截至图标
const img = new Image();
img.src = pointImageURLs[4];
img.onload = () => {
ctx.drawImage(img, pointXs, pointYs,28 * dpr,28 * dpr);
ctx.fill()
}
}else{
// 不展示任何图标
return
}
}else {
const img = new Image();
img.src = pointImageURLs[iconIndex.value];
img.onload = () => {
ctx.drawImage(img, pointXs, pointYs,28 * dpr,28 * dpr);
ctx.fill()
}
}
// //------------绘制7天截至矩形盒子
let showBox=show7Days.value(point)
if(showBox==true){
let diaheight = setCanvasDiaHeight(point.name)
ctx.beginPath()
// 创建渐变对象
const gradient = ctx.createLinearGradient(0, 0, 0, 18 * dpr);
gradient.addColorStop(0, 'rgba(255,255,255,1)'); // 渐变起始颜色
gradient.addColorStop(1, 'rgba(255,226,220,1)'); // 渐变结束颜色
// 设置渐变为填充样式
ctx.fillStyle = gradient;
drawRoundedRect(ctx, point.pintX - 50 * dpr, point.pintY - diaheight* dpr - 3 * dpr, 76 * dpr, 18 * dpr, 10 * dpr,0)
ctx.fill()
ctx.strokeStyle = 'white'
ctx.stroke();
}else {
return
}
// //------------绘制7天截至矩形盒子
let show=show7Days.value(point)
if(show==true){
ctx.beginPath()
let diaheight = setCanvasDiaHeight(point.name)
ctx.fillStyle = '#F94E2A'
ctx.font = `${12 * dpr}px AlibabaPuHuiTi` // 设置字体大小和颜色
ctx.fillText('7天截止', point.pintX - (4 * dpr), point.pintY - (diaheight * dpr) + (7 * dpr) )
ctx.fill()
}else {
return
}
})
}
}
}
const goback = () => {
router.go(-1)
}
const showDialog=()=>{
show.value=true
dialogConfimVisi.intro=props.basicInfo.introduction
dialogConfimVisi.dialogVisible = true
}
const dialogCofinmClose = () => {
dialogConfimVisi.dialogVisible = false
}
onMounted(() => {
canvasFun()
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', function (e) {
const radius = 50
// 遍历所有标注点
for (var i = 0; i < newArr.value.length; i++) {
var point = newArr.value[i]
// 计算点击坐标与标注点坐标的距离
var dx = e.offsetX - (point.pintX / dpr)
var dy = e.offsetY - (point.pintY / dpr) + 50
var distance = Math.sqrt(dx * dx + dy * dy)
//如果距离小于半径,则认为点击在标注点上
if (distance < radius) {
// 这里可以添加点击标注点后的处理逻辑
// console.log(points.value[i], '===')
// alert('Clicked point: '+point.name)
let obj = showBtnName.value(point)
let isClick=point.isClick
if(obj.label=='过期'){
return
}
// 未按顺序解锁,提示按顺序参与
if(isClick==false){
Toast('按顺序参与')
return
}
goSecondPage(point, obj.label, obj.showClass)
}
}
})
window.addEventListener('resize', canvasFun);
})
watch(
() => props.basicInfo,
(val) => {
if (val && val.backgroundImage) {
console.log('进来了')
canvasFun()
}
},
{
deep: true,
immediate: true,
}
)
onBeforeUnmount(() => {
window.removeEventListener('resize', canvasFun)
})
</script>
<style lang="less" scoped>
.interactiveActivitySecond {
position:relative;
width: 100vh;
height: 100vw;
transform-origin: 0 0;
transform: rotateZ(90deg) translateY(-100%);
background: #3C7D61;
// padding-top: 12px;
#canvas {
position: relative;
// pointer-events: none; /* 阻止鼠标事件 */
touch-action: none; /* 阻止触摸事件 */
position:absolute;
top:20px;
left:0;
}
.arrow-left {
// display: flex;
// align-items: center;
position: absolute;
top: 12px;
left: 14px;
// > img {
// width: 9px;
// height: 14px;
// }
display: inline-block;
width: 32px;
height: 32px;
background: url('@/assets/images/second-image/second-back.svg') no-repeat
center / contain;
background-color: rgba(255, 255, 255, 0.1);
font-size: 18px;
margin-right: 10px;
border-radius: 12px;
background-size: 7px 12px;
vertical-align: middle;
cursor: pointer;
z-index:1;
}
.intro {
position: absolute;
top: 12px;
left: 54px;
display: inline-block;
width: 32px;
height: 32px;
background: url('@/assets/images/second-image/intro.png') no-repeat
center / contain;
background-color: rgba(255, 255, 255, 0.1);
font-size: 18px;
margin-right: 10px;
border-radius: 12px;
background-size: 12px 14px;
vertical-align: middle;
cursor: pointer;
z-index:1;
}
.vanOverlays {
position: absolute!important;
top:0px!important;
left:0px!important;
background:rgba(0,0,0,0.3)!important;
width:100vh!important;
}
}
</style>