Canvas实现数字雨和放大镜效果

前言

之前在学习的时候没有了解过Canvas的使用,趁着有空来学习一下Canvas,顺便实现两个简单的效果,数字雨和放大镜效果。后面有完整代码。

正文

还是先来看看效果

  • 数字雨
  • 放大镜

数字雨

我认为数字雨的核心在于单条数字雨生成数字雨移动

事前先准备几个函数,方便我们的操作,随机数可以用来生成数字雨的坐标,文字的大小以及移动的速度。

ts 复制代码
  /** 生成随机数*/
  const createRandomNum = useCallback((min: number, max: number) => {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }, [])
  
  
  /**
  * @description 创建01字符串
  * @returns 01字符串
  */
  function generateRandomString() {
    const characters = '01';
    const len = Math.floor(Math.random() * (60 - 45 + 1)) + 45;
    let randomString = '';

    for (let i = 0; i < len; i++) {
      const randomIndex = Math.floor(Math.random() * characters.length);
      randomString += characters[randomIndex];
    }
    return randomString;
  }

生成单条数字雨

单条的样式就出来了 由于我们接下来要对它进行移动,那我们必须记录数字雨的相关信息,然后对坐标进行修改,最后再绘制就可以了。

ts 复制代码
interface List {
  x: number,
  y: number,
  text: string,
  fontSize: number,
  width: number,
  speed: number,
}

在我们每生成一条数字雨时就去记录对应数据 再结合requestAnimationFrame,我们的动画效果就出来了

放大镜

放大镜比数字雨还简单,一共就两步,获取有效的鼠标坐标drawImage切图并绘制

代码

数字雨

tsx 复制代码
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import './DigitalRain.scss'

interface List {
  x: number,
  y: number,
  text: string,
  fontSize: number,
  width: number,
  speed: number,
}

export default function Rain() {
  const canvasDom = useRef<any>(null)
  const canvasCtx = useRef<any>(null)
  const [width, setWidth] = useState(0)
  const [height, setHeight] = useState(0)
  /** 数字雨总数*/
  const amount = useRef(100);
  /** 已存在的数字雨*/
  const dataList = useRef<List[]>([])
  const animation = useRef<any>(null)


  /** 设置canvas的宽高*/
  const setCanvasSize = useCallback(() => {
    const screenWidth = window.innerWidth;
    const screenHeight = window.innerHeight;

    setHeight(screenHeight)
    setWidth(screenWidth)
  }, [])

  /** 生成随机数*/
  const createRandomNum = useCallback((min: number, max: number) => {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }, [])

  useEffect(() => {
    if (canvasDom.current === null) {
      return
    }
    setCanvasSize()
    canvasCtx.current = canvasDom.current.getContext('2d');

    window.addEventListener('resize', setCanvasSize)
    window.addEventListener('scroll', setCanvasSize)
  }, [])

  useEffect(() => {
    canvasCtx.current.fillStyle = 'black';
    canvasCtx.current.fillRect(0, 0, width, height);

    cancelAnimationFrame(animation.current);
    draw()
  }, [width, height])

  /**
  * @description 创建01字符串
  * @returns 01字符串
  */
  function generateRandomString() {
    const characters = '01';
    const len = Math.floor(Math.random() * (60 - 45 + 1)) + 45;
    let randomString = '';

    for (let i = 0; i < len; i++) {
      const randomIndex = Math.floor(Math.random() * characters.length);
      randomString += characters[randomIndex];
    }
    return randomString;
  }

  /** 单条数字雨生成*/
  const drawSeparateLine = useCallback((fontSize: number, width: number, x: number, y: number, text: string) => {
    canvasCtx.current.font = `bold ${fontSize}px Arial`
    let grd = canvasCtx.current.createLinearGradient(x, y, x + width, y);
    grd.addColorStop(0, "aqua");
    grd.addColorStop(1, "transparent");
    canvasCtx.current.fillStyle = grd
    canvasCtx.current.shadowColor = "aqua";  // 设置阴影颜色
    canvasCtx.current.shadowBlur = 20;  // 设置阴影的模糊程度
    canvasCtx.current.fillText(text, x, y)
  }, [])

  /**
  * @description 记录数据
  * @param x 起始坐标 {number} 
  * @param y 起始坐标 {number} 
  * @param text 文字 {string} 
  * @returns
  */
  const recordData = useCallback((x: number, y: number, text: string) => {
    const fontSize = createRandomNum(17, 24)
    const speed = createRandomNum(2, 4)
    let textOne = canvasCtx.current.measureText(text);
    drawSeparateLine(fontSize, textOne.width, x, y, text)

    dataList.current.push({
      x,
      y,
      text,
      fontSize,
      width: textOne.width,
      speed
    })
  }, [])

  /** 画整个页面*/
  const draw = useCallback(() => {
    canvasCtx.current.clearRect(0, 0, width, height)
    canvasCtx.current.fillStyle = 'black';
    canvasCtx.current.fillRect(0, 0, width, height);

    /** 移动*/
    for (let i = 0; i < dataList.current.length; i++) {
      let item = dataList.current[i];
      drawSeparateLine(item.fontSize, item.width, item.x - item.speed, item.y, item.text)
      dataList.current[i] = {
        ...dataList.current[i],
        x: item.x - item.speed
      }
    }

    /**  增加新的*/
    const maxWidth = window.innerWidth * 3;
    const minWidth = window.innerWidth;
    const maxHeight = window.innerHeight;
    const minHeight = 0;
    for (let i = 0; i < amount.current - dataList.current.length; i++) {
      let x = createRandomNum(minWidth, maxWidth)
      let y = createRandomNum(minHeight, maxHeight)
      recordData(x, y, generateRandomString())
    }

    /** 去除旧的*/
    let list: number[] = [];
    for (let i = 0; i < dataList.current.length; i++) {
      if (dataList.current[i].x + dataList.current[i].width <= 0) {
        list.push(i)
      }
    }
    dataList.current = dataList.current.filter((item, ind) => {
      return !list.includes(ind)
    })

    animation.current = requestAnimationFrame(draw);
  }, [width, height])

  return (
    <>
      <div className='rain'>
        <canvas ref={canvasDom}
          width={width}
          height={height}
        ></canvas>
      </div>
    </>
  )
}

放大镜

tsx 复制代码
/** 放大镜*/
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import img1 from '../assets/imgs/1.jpg'
import './MagnifyingGlass.scss'

export default function Canvas() {
  const frame = useRef<any>(null)
  const canvasDom = useRef<any>(null)
  const magnifyCanvasDom = useRef<any>(null)
  const canvasCtx = useRef<any>(null)
  const magnifyCanvasCtx = useRef<any>(null)
  const magnifyingGlassSize = useRef(40)

  const [top, setTop] = useState(0);
  const [left, setLeft] = useState(0);

  const initLocation = useRef<any>({
    x: 0,
    y: 0,
    minX: 0,
    maxX: 0,
    minY: 0,
    maxY: 0,
    size: 0,
  })


  const setInitPointer = useCallback(() => {
    let info = canvasDom.current.getBoundingClientRect()
    initLocation.current = {
      x: info.x,
      y: info.y,
      minX: info.x,
      maxX: info.x + info.width - magnifyingGlassSize.current,
      minY: info.y,
      maxY: info.y + info.height - magnifyingGlassSize.current,
    }
  }, [])

  /** 初始化,渲染图片*/
  useEffect(() => {
    if (canvasDom.current == null) {
      return
    }
    canvasCtx.current = (canvasDom.current).getContext('2d');
    magnifyCanvasCtx.current = (magnifyCanvasDom.current).getContext('2d');
    setInitPointer()
    let img = new Image();
    img.src = img1;
    img.onload = () => {
      const canvasWidth = canvasDom.current.width;
      const canvasHeight = canvasDom.current.height;
      const imageWidth = img.width;
      const imageHeight = img.height;
      const scale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight);
      const scaledWidth = imageWidth * scale;
      const scaledHeight = imageHeight * scale;

      canvasCtx.current.drawImage(img, 0, 0, scaledWidth, scaledHeight)

      magnifyCanvasCtx.current.drawImage(
        canvasDom.current,
        0,
        0,
        magnifyingGlassSize.current,
        magnifyingGlassSize.current,
        0,
        0,
        300,
        300
      );
    }

    frame.current.addEventListener('mousemove', onMousemove)
    window.addEventListener('resize', setInitPointer)
    window.addEventListener('scroll', setInitPointer)

    return () => {
      frame.current.removeEventListener('mousemove', onMousemove)
      window.removeEventListener('resize', setInitPointer)
      window.removeEventListener('scroll', setInitPointer)
    }
  }, [])

  const onMousemove = useCallback((e: MouseEvent) => {
    let x = e.x;
    let y = e.y;
    let dataY = y - initLocation.current.y - magnifyingGlassSize.current / 2;
    //判断边界
    if (dataY < initLocation.current.minY) {
      dataY = initLocation.current.minY
    } else if (dataY > initLocation.current.maxY) {
      dataY = initLocation.current.maxY
    }
    setTop(dataY)

    //判断边界
    let dataX = x - initLocation.current.x - magnifyingGlassSize.current / 2;
    if (dataX < initLocation.current.minX) {
      dataX = initLocation.current.minX
    } else if (dataX > initLocation.current.maxX) {
      dataX = initLocation.current.maxX
    }
    setLeft(dataX)

    /** 切图*/
    magnifyCanvasCtx.current.drawImage(
      canvasDom.current,
      dataX,
      dataY,
      magnifyingGlassSize.current,
      magnifyingGlassSize.current,
      0, 0,
      300, 300
    );
  }, [])


  return (
    <>
      <div ref={frame} style={{
        display: 'inline-block'

      }}>
        <canvas
          className='glass'
          ref={canvasDom} width={300} height={300}>
        </canvas>

        <div
          style={{
            position: 'fixed',
            zIndex: 0,
            top,
            left,
            width: `${magnifyingGlassSize.current}px`,
            height: `${magnifyingGlassSize.current}px`,
            background: 'yellow',
            opacity: '.2'
          }}>
        </div>
      </div>

      <canvas
        ref={magnifyCanvasDom} width={300} height={300}>
      </canvas>
    </>
  )
}

结语

分享结束

相关推荐
阿伟来咯~36 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端41 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱44 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js