解决移动端canvas 绘制的图片模糊的问题

之前前面写过一遍有关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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值