current position:Home>How to create a high-performance full screen red envelope rain

How to create a high-performance full screen red envelope rain

2022-04-29 20:19:50Mirai white

Preface

The new demand needs to make a red envelope rain game , Know everything. , The specific logic is shown in the figure :

23

Calm down and analyze , It's nothing more than

  1. Red envelope falling animation
  2. Click red envelope +1 Animation
  3. Play a coupon at the end of the game

The core function point is the whereabouts of the red envelope , The first reaction was . Or use canvas、 Or use CSS3. Since I was born , I've always seen :

a large number of DOM Nodes can lead to vertical performance degradation ,div Do the animation , It will constantly trigger the reflow and redrawing of the browser , Can the performance be good ? and canvas Just one node , Performance carrying .

And I'm , I think so too . What's more? canvas Than CSS3 Animation is much more complicated . In terms of universal rationality , The more complicated things , The better the performance .

Canvas Method

Initialize canvas

Very plain code , Incoming width and height are canvas Set the corresponding width and height , Otherwise, the width and height is the width and height of the viewport . hold canvas The context and width and height are saved for subsequent use .

const redPacketsCanvasRef = ref<HTMLCanvasElement>()
const redPacketsContext = ref<ReturnType<typeof initCanvas>>()

function initCanvas( canvasElement: HTMLCanvasElement, params: { width?: number; height?: number } = {} ) {
  const canvasWidth = params.width ?? window.innerWidth
  const canvasHeight = params.height ?? window.innerHeight

  canvasElement.width = canvasWidth
  canvasElement.height = canvasHeight

  return {
    ctx: canvasElement.getContext('2d')!,
    params: {
      width: canvasWidth,
      height: canvasHeight,
    },
  }
}

onMounted(async () => {
  if (redPacketsCanvasRef.value) {
    redPacketsContext.value = initCanvas(redPacketsCanvasRef.value)
  }
})
 Copy code 

Draw a red envelope element

Each red envelope will have the following properties , Used to control all behaviors of red envelopes .

export type RedPacketType = {
  /**  Red envelopes  id */
  id: number
  /** x Axis position  */
  x: number
  /** y Axis position  */
  y: number
  /**  The width of the red envelope  */
  width: number
  /**  Red envelope height  */
  height: number
  /**  Red envelope falling speed  */
  speed: number
  /**  The maximum rotation angle of the red envelope  */
  rotate: number
  /**  Red envelope rotation speed  */
  rotateSpeed: number
  /**  Red envelope picture elements  */
  imageEl: HTMLImageElement
  /**  The color value judged when the auxiliary red envelope is hit  */
  subHitColor: string
}
 Copy code 

By the way, recreate some false data .

type MockConfigType = {
  width?: number
  height?: number
  speed?: number
  imageUrl?: string
  maxRotateDeg?: number
  renderTime?: number
}
const props = withDefaults(defineProps<MockConfigType>(), {
  width: 80,
  height: 101,
  speed: 3,
  imageUrl: 'game-red-packet.png',
  maxRotateDeg: 30,
  renderTime: 1200,
})
 Copy code 

When generating red envelopes , We need to do something “ reasonable ” To prepare ( Some properties will be explained later ).

  1. We hope the red envelope has a unique id, So simply Math.random() * 1e18 Generate a pseudo-random number as id value .
  2. The red envelope must not be from the screen (0, 0) The coordinates of appear directly , This will appear very abrupt . therefore y The shaft needs to move up properly 1.5 The distance of the height of a red envelope , Reduce the sense of disobedience when falling .
  3. Red envelope rain must be the movement of more than one red envelope , So you need to save the generated red envelope into an array , In the future, each red envelope will be drawn uniformly to achieve “ move ” The effect of .
const redPacketList = ref<Record<number | string, RedPacketType>>({})

async function createRedPacketItemObject({ x, y }: { x: number; y: number }) {
  const id = Math.random() * 1e18
  const { width, height, imageUrl, speed } = props
  const config: RedPacketType = {
    id,
    x,
    y: y - height * 1.5,
    width,
    height,
    imageEl: await loadImages(imageUrl),
    speed,
    rotate: 0,
    rotateSpeed: Math.round(Math.random()) ? 0.2 : -0.2,
    subHitColor: getHashColor(Object.values(redPacketList.value).map((x) => x.subHitColor ?? '')),
  }

  redPacketList.value[id] = config

  return config
}
 Copy code 

Change it appropriately y Position of the shaft , Use CanvasRenderingContext2D.drawImage() Draw a red envelope Kangkang first .

onMounted(async () => {
  if (redPacketsCanvasRef.value) {
    redPacketsContext.value = initCanvas(redPacketsCanvasRef.value)

    const { imageEl, x, y, width, height } = await createRedPacketItemObject({ x: 40, y: 200 })
    redPacketsContext.value.ctx.drawImage(imageEl, x, y, width, height)
  }
})
 Copy code 
image-20220421011544597

Red envelope falling animation

The red envelope has been drawn , The next step is to make it fall .

The movement of red envelopes is actually The canvas is frequently erased and every red envelope element , Each drawing changes the of each red envelope in the next drawing y Axis position , In this way, the red envelope seems to be falling all the time . On this basis , We just need to reach 4 individual Consensus The core approach :

  1. Use CanvasRenderingContext2D.clearRect() Empty the canvas .
  2. Traverse redPacketList Array Every red envelope element in , adopt CanvasRenderingContext2D.drawImage() Draw each red envelope , After each red envelope is drawn , Change the of the current red envelope y The axis value is The falling speed of the red envelope + At present y Axis value redPacketList.value[id].y = y + speed.
  3. Judge whether the red envelope is no longer in the visible area if (redPacketList.value[id].y > canvasParams.height), Remove objects directly when not in the visible area , Avoid the page jam caused by the accumulation of red envelopes .
  4. Use requestAnimationFrame Let the function continue to execute , Finish the animation .
const rafRedPacketsMove = ref<number>()

async function renderRedPacketItem() {
  if (redPacketsContext.value) {
    const { ctx, params: canvasParams } = redPacketsContext.value
    ctx.clearRect(0, 0, canvasParams.width, canvasParams.height)

    for (const [id, redPacket] of Object.entries(redPacketList.value)) {
      const { y, speed } = redPacket
      const { imageEl, x, width, height } = redPacket
      ctx.drawImage(imageEl, x, y, width, height)
      redPacketList.value[id].y = y + speed

      if (redPacketList.value[id].y > canvasParams.height) {
        console.log(`[hidden-delete-${id}]`, redPacketList.value[id])
        delete redPacketList.value[id]
      }
    }

    rafRedPacketsMove.value = requestAnimationFrame(renderRedPacketItem)
  }
}

onMounted(async () => {
  if (redPacketsCanvasRef.value) {
    redPacketsContext.value = initCanvas(redPacketsCanvasRef.value)

    await createRedPacketItemObject({ x: 40, y: 0 })
    renderRedPacketItem()
  }
})
 Copy code 
1

Red envelope self rotation animation

Usually , The drop of red envelopes will be a little monotonous , So we may need to rotate the red envelope slightly to the left and right when it falls ( In fact, it's fun for others to watch the red rain ).

but canvas The rotation is around the upper left corner of the canvas (0,0) Start rotating , and css The default center rotation is different , So we need to simulate css The center of rotation . Just drive ~

  1. avoid “ Pollution ” Subsequent operations on the canvas , adopt CanvasRenderingContext2D.clearRect() Put the current state on the stack , preservation canvas All status Methods .
  2. adopt CanvasRenderingContext2D.translate(), take canvas According to the original x The horizontal direction of the point 、 The original y The point is perpendicular Move to the center of the red envelope .
  3. adopt CanvasRenderingContext2D.rotate() Rotate the canvas to the desired angle , At this point, the drawing environment has been rotated .
  4. Change the coordinates of the center point of the drawing environment , Back to the origin , Draw red envelope pictures .
  5. Change the of the next rotation rotate value .
  6. adopt CanvasRenderingContext2D.restore(), recovery To the latest save canvas The method of all States , That is the translate rotate Restore your state .

Maybe the text is not easy to understand the rotating part , Here let's use PS Power , Focus on 180deg Rotate as a chestnut .

3

Because the drawing of the picture needs to be completed when rotating , So it needs to be transformed renderRedPacketItem function , In the method ctx.drawImage(imageEl, x, y, width, height) Draw pictures and give them to function rotateRedPicketElement draw .

async function renderRedPacketItem() {
  if (redPacketsContext.value) {
    const { ctx, params: canvasParams } = redPacketsContext.value
    ctx.clearRect(0, 0, canvasParams.width, canvasParams.height)

    for (const [id, redPacket] of Object.entries(redPacketList.value)) {
      const { y, speed } = redPacket
      rotateRedPicketElement(ctx, redPacket)
      redPacketList.value[id].y = y + speed

      if (redPacketList.value[id].y > canvasParams.height) {
        console.log(`[hidden-delete-${id}]`, redPacketList.value[id])
        delete redPacketList.value[id]
      }
    }

    rafRedPacketsMove.value = requestAnimationFrame(renderRedPacketItem)
  }
}

function rotateRedPicketElement(ctx: CanvasRenderingContext2D, params: RedPacketType) {
  function rotateThresholdValue(rotate: number) {
    const { maxRotateDeg } = props
    if (Math.abs(rotate) >= maxRotateDeg) {
      return rotate > 0 ? maxRotateDeg : -maxRotateDeg
    }
    return rotate
  }

  ctx.save()
  const { id, x, y, width, height, rotate, imageEl, rotateSpeed } = params
  const centerPointPosition = {
    x: x + width / 2,
    y: y + height / 2,
  }
  ctx.translate(centerPointPosition.x, centerPointPosition.y)
  ctx.rotate((rotate * Math.PI) / 180)
  ctx.translate(-centerPointPosition.x, -centerPointPosition.y)
  ctx.drawImage(imageEl, x, y, width, height)

  redPacketList.value[id].rotate = rotateThresholdValue(rotate + rotateSpeed)

  ctx.restore()
}
 Copy code 
4

Red envelope click event judgment 1( Axis calculation )

Generated red envelope elements , Naturally, I want users to do something after clicking , So click events are necessary .Canvas We can't directly draw a “ Elements ” Listen for click events , We can only monitor Canvas Click events for , Determine the click according to the coordinate axis position .

Because we saved the of each red envelope element x, y Axis coordinates and the width and height of the picture , So we can easily write a piece of code whether we hit the red envelope .

function clickCanvas(e: MouseEvent, ctx: CanvasRenderingContext2D) {
  const { offsetX, offsetY } = e

  for (const [id, redPacketItem] of Object.entries(redPacketList.value)) {
    const { width, height, x, y } = redPacketItem
    const point = {
      x1: x,
      y1: y,
      x2: x + width,
      y2: y + height
    }
    const isHandle = offsetX >= point.x1 && offsetX <= point.x2 && offsetY >= point.y1 && offsetY <= point.y2
    if (isHandle) {
      delete redPacketList.value[id]
    }
  }
}

onMounted(async () => {
  if (redPacketsCanvasRef.value) {
    redPacketsContext.value = initCanvas(redPacketsCanvasRef.value)

    await createRedPacketItemObject({ x: 40, y: 0 })
    renderRedPacketItem()

    redPacketsCanvasRef.value.addEventListener('click', (e) => clickCanvas(e, redPacketsContext.value!.ctx))
  }
})
 Copy code 
5

At this time, a pretty boy will say ,“ If the red envelopes overlap , Then how do you judge which one you clicked ?”

In fact, it's also very easy to solve , because canvas Pictures drawn , All are “ Layer by layer ” Of , Therefore, the red envelope generated later must be on the red envelope generated earlier . Then we just need to add one to each red envelope zIndex attribute , Every time a red envelope is generated zIndex++. Clicking time , You can get the information of all hit red packets , to zIndex The maximum red envelope , It's the red envelope we can see with our naked eyes .

const redPacketAccumIndex = ref(1)

async function createRedPacketItemObject({ x, y }: { x: number; y: number }) {
  const id = Math.random() * 1e18
  const { width, height, imageUrl, speed } = props
  const config: RedPacketType = {
    id,
    x,
    y: y - height * 1.5,
    width,
    height,
    imageEl: await loadImages(imageUrl),
    speed,
    rotate: 0,
    rotateSpeed: Math.round(Math.random()) ? 0.2 : -0.2,
    subHitColor: getHashColor(Object.values(redPacketList.value).map((x) => x.subHitColor ?? '')),
    //  When you create a red envelope, add... To each red envelope  zIndex value 
    zIndex: redPacketAccumIndex.value++,
  }

function clickCanvas(e: MouseEvent, ctx: CanvasRenderingContext2D) {
  const { offsetX, offsetY } = e
  /**  All red packets hit in the range  */
  const isClickArray = []

  for (const [id, redPacketItem] of Object.entries(redPacketList.value)) {
    const { width, height, x, y } = redPacketItem
    const point = {
      x1: x,
      y1: y,
      x2: x + width,
      y2: y + height
    }
    const isHandle = offsetX >= point.x1 && offsetX <= point.x2 && offsetY >= point.y1 && offsetY <= point.y2
    if (isHandle) {
      isClickArray.push(redPacketItem)
    }
  }
  
  if (isClickArray[0]) {
    const sortArray = isClickArray.sort((a, b) => b.zIndex - a.zIndex)
    const { id } = sortArray[0]
    delete redPacketList.value[id]
  }
}
 Copy code 
6

It looks like there's nothing wrong , But it always feels ... The red envelope has been rotating , Spin, you know , You can't judge with the simple formula of appeal , You need to recalculate the hit area of each pixel of the red envelope according to the rotation angle . At present, the judgment of click area is only on the green block , The angle after rotation is not recalculated .

7

Red envelope click event judgment 2( Simulate the click area )

The idea of simulating the click area is very simple and rough . We just need to draw the red envelope , Draw one of the same size at the same time 、 Graphics with unique colors in the location . When obtaining the pixels of the click area , Find a graphic with the same color . But because it can't affect the original drawing , We need to create an auxiliary simulation click canvas Cover the red envelope canvas On , Set up css The transparency of the style is 0 avoid “ Pollution ” Red envelopes canvas, To complete our click behavior .

First , We need a way to generate random colors getHashColor To give each red envelope a unique color .

export function getRandomColor() {
  const r = Math.round(Math.random() * 255)
  const g = Math.round(Math.random() * 255)
  const b = Math.round(Math.random() * 255)
  return `rgb(${r},${g},${b})`
}

export function getHashColor(hashColorList: string[]) {
  let hashColor = getRandomColor()
  for (;;) {
    if (!hashColorList.includes(hashColor)) {
      return hashColor
    }
    hashColor = getRandomColor()
  }
}
 Copy code 

secondly , We need an auxiliary click canvas( For ease of understanding , Set the transparency to 0.7).

<script>
const hitSubCanvasRef = ref<HTMLCanvasElement>()
const hitSubCanvasContext = ref<ReturnType<typeof initCanvas>>()
</script>

<template>
  <canvas ref="redPacketsCanvasRef" />
  <canvas ref="hitSubCanvasRef" style="opacity: 0.7" />
</template>

<style>
canvas {
  position: absolute;
  top: 0;
  left: 0;
}
</style>
 Copy code 

next , We mainly transform rotateRedPicketElement The method of drawing . While drawing the red envelope , Let's draw the same position 、 Large colored rectangle .

function rotateRedPicketElement( ctx: CanvasRenderingContext2D, params: RedPacketType, hitCtx: CanvasRenderingContext2D ) {
  function rotateThresholdValue(rotate: number) {
    const { maxRotateDeg } = props
    if (Math.abs(rotate) >= maxRotateDeg) {
      return rotate > 0 ? maxRotateDeg : -maxRotateDeg
    }
    return rotate
  }

  ctx.save()
  const { id, x, y, width, height, rotate, imageEl, rotateSpeed, subHitColor } = params
  const centerPointPosition = {
    x: x + width / 2,
    y: y + height / 2,
  }
  ctx.translate(centerPointPosition.x, centerPointPosition.y)
  ctx.rotate((rotate * Math.PI) / 180)
  ctx.translate(-centerPointPosition.x, -centerPointPosition.y)
  ctx.drawImage(imageEl, x, y, width, height)
  ctx.restore()

  hitCtx.save()
  hitCtx.translate(centerPointPosition.x, centerPointPosition.y)
  hitCtx.rotate((rotate * Math.PI) / 180)
  hitCtx.translate(-centerPointPosition.x, -centerPointPosition.y)
  hitCtx.fillStyle = subHitColor
  hitCtx.fillRect(x, y, width, height)
  hitCtx.restore()

  redPacketList.value[id].rotate = rotateThresholdValue(rotate + rotateSpeed)
}
 Copy code 

Last , Click for auxiliary canvas Add click event .

When the user clicks , We just need to listen and click canvas Of click event , Get offsetX, offsetY coordinate . Re pass CanvasRenderingContext2D.getImageData() Get rgb value , from redPacketList According to the rgb Value to judge which red envelope is clicked .

function hitSubCanvas(e: MouseEvent, ctx: CanvasRenderingContext2D) {
  const { offsetX, offsetY } = e

  const [r, g, b] = ctx.getImageData(offsetX, offsetY, 1, 1).data
  const rgb = `rgb(${r},${g},${b})`

  const hitRedPacketItem = Object.values(redPacketList.value).find((x) => x.subHitColor === rgb)

  if (hitRedPacketItem) {
    console.log(`[hit-delete-${hitRedPacketItem.id}]`, hitRedPacketItem)
    delete redPacketList.value[hitRedPacketItem.id]
  }
}

onMounted(async () => {
  if (redPacketsCanvasRef.value && hitSubCanvasRef.value) {
    redPacketsContext.value = initCanvas(redPacketsCanvasRef.value)
    hitSubCanvasContext.value = initCanvas(hitSubCanvasRef.value)

    await createRedPacketItemObject({ x: 40, y: 0 })
    renderRedPacketItem()

    hitSubCanvasRef.value.addEventListener('click', (e) =>
      hitSubCanvas(e, hitSubCanvasContext.value!.ctx)
    )
  }
})
 Copy code 

Because the hit is judged by the color , Therefore, there is no need to worry about the problem of click misjudgment caused by red envelope overlap .

8

The location and whereabouts of the generated red envelope

The animation logic of a single red envelope has been processed from generation to whereabouts , Next, you need to deal with the logic of multiple red packets .

Red envelope location

We hope red envelopes can fall regularly , Here we just need to meet :

  1. Do not overlap with the red envelope of the last landing , And each group of red envelopes can be displayed in every part of the screen x Can appear once in each column .
  2. Does not appear on the boundary , Avoid inconvenient clicks by users , At the same time, we also consider the situation of curved screen mobile phones ;

Direct mental arithmetic may be a little troublesome , Here, the calculation function is written directly by inverse deduction computedXPoint. Set the screen width to 375px, The width of the red envelope is 60px, Set the inside margin of the screen 20px. At this point you can get , The direct spacing of each red envelope is 19px.

image-20220427004742683

It's not hard to see , Screen margin 20px There must be . The position of the first red envelope is sp20 + m19、 The second red envelope position is sp20 + m19 + ( w60 + m19 )、 The third red envelope position is sp20 + m19 + ( w60 + m19 ) + ( w60 + m19 ), And so on .

image-20220427005927365

The law is already obvious , It seems that you can write it smoothly .

function computedXPoint() {
  const { split, screenPadding, width } = props
  const maxScreenWidth = window.innerWidth - screenPadding * 2
  const maxFreeSpace = maxScreenWidth - width * split
  const marginSpace = maxFreeSpace / (split + 1)
  return Object.keys([...Array(split)]).map(Number).map(x => screenPadding + marginSpace + (width + marginSpace) * x)
}
 Copy code 

Turn off the spin , Generate a look at the location .

9

Way of falling

There are positions , Next comes the falling animation . The way of falling also needs to meet several conditions :

  1. Only one red envelope will be generated at a time in the same line , Avoid generating red envelopes too neatly to reduce interest .
  2. The red envelope cannot be directly descending in order with the generated position array , Need to disrupt the array , Used to add interest .

The words of thinking are also very rough , Generate the coordinate array of the number of red packets in advance , Use the timer to keep adding red packets to the array ,canvas Be responsible for drawing .

The method of scrambling arrays doesn't need to be too rigorous , Use sorting and random numbers to directly disrupt

function getRandomArray(array: number[]) {
  return array.sort(() => 0.5 - Math.random())
}
 Copy code 

be used for canvas The red envelope drawing has been animated by the red envelope falling renderRedPacketItem Function on the red packet array redPacketList Keep drawing . So we just need to write a timer without brain , Regularly add red packets to the array , That is, keep executing createRedPacketItemObject Function to create a red envelope .

const pointList = computedXPoint()
const { split, totalPackets } = props
const allPointList = [...Array(Math.ceil(totalPackets / split))].map(() => getRandomArray(pointList)).flat().splice(0, totalPackets).map(Math.ceil)

let index = 0
let timer: number | null = null
createRedPacketItemObject({ x: allPointList[index++], y: 0 })
timer = setInterval(() => {
  if (index >= totalPackets - 1) {
    clearInterval(timer!)
  }
  createRedPacketItemObject({ x: allPointList[index++], y: 0 })
}, 700)
 Copy code 
10

Capsize , Some models have serious browser jamming

Xiaomi browser Quark browser All are chromium kernel , But there is an obvious frame loss . Look, there may only be 10 Red envelope rain of frame , There is no disturbance in my heart . Since all of them are chromium, Plug in USB Direct real machine debugging .

Because the tester has only 4G The shipment to save , So I think there may be a performance problem . During this period, various optimization schemes were tried : Although there is only one red envelope element in the picture, it is still rendered off screen 、 All parameters are rounded to avoid floating point numbers . But there was no obvious effect , The card is still the card ) :

41

Google How do you say it

go

And we are

image-20220427013636073

Have no alternative , Use Chrome Opened the game , I can't see carton at all ( I'm tired ).

Although the frame rate is not very stable , But it's not stuck !

It looks like the browser shell at the same time , The priority of the scheduling system has been changed ? Although the feeling may be that the code is too floating , But a requestAnimationFrame It's not as serious as falling frames .

There's no way ,canvas Caton has seriously and greatly affected the user experience , If it can't solve the Caton problem, it can only be dropped directly . use @keyframes I ran an animation test machine . Um. ,60 Full frame , There is not a trace of Caton .

CSS3 Method

Use CSS3 Write the red envelope rain game , be relative to canvas Come on , It's a dimensionality reduction attack .

alike , We also need a “ Canvas Container ”

<script setup lang="ts">
const gameContainerRef = ref<HTMLElement>()
</script>

<template>
  <div class="game-container" ref="gameContainerRef"></div>
</template>

<style lang="scss">
.game-container {
  width: 100vw;
  height: 100vh;
  margin: 0 auto;
  overflow: hidden;
}
</style>
 Copy code 

Create a red envelope element

The same false data , Similarly, each red envelope needs to have a unique id

<script setup lang="ts">
function createRedPacket(xPonit: number) {
  const id = Math.random() * 1e18
  redPacketList.value.push(id)

  const { width, height, imageUrl } = props
  const packetEl = document.createElement('img')
  packetEl.src = new URL(`../assets/${imageUrl}`, import.meta.url).href
  packetEl.classList.add('red-packet-img')
  packetEl.id = 'packet-' + id
  packetEl.style.width = width + 'px'
  packetEl.style.height = height + 'px'
  packetEl.style.left = xPonit + 'px'

  gameContainerRef.value!.appendChild(packetEl)
}
    
onMounted(async () => {
  createRedPacket(20)
})
</script>

<template>
  <div class="game-container" ref="gameContainerRef"></div>
</template>

<style lang="scss">
.game-container {
  width: 100vw;
  height: 100vh;
  margin: 0 auto;
  overflow: hidden;

  .red-packet-img {
    position: absolute;
    top: 0;
    left: 0;
    // transform: translateY(-100%);
    font-size: 0;
  }
}
</style>
 Copy code 
image-20220428004518780

Red envelopes @keyframes Falling animation

Use CSS3 The animation is very simple , We just need to create one @keyframes Keyframes to Specify the start and end states of the animation . When each red envelope is created , Use it directly animation Create a red envelope animation . No calculation at all , No brain pile API that will do .

<style lang="scss">
.game-container {
  width: 100vw;
  height: 100vh;
  margin: 0 auto;
  overflow: hidden;

  .red-packet-img {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(-100%);
    font-size: 0;
    animation: down 3s linear forwards;
  }

  @keyframes down {
    0% {
      transform: translateY(-100%);
    }

    100% {
      transform: translateY(110vh);
    }
  }
}
</style>
 Copy code 
11

Red envelope self rotation animation

Rotation animation consists of transform: rotate(); To complete , But because of the direction of rotation ( Pros and cons ) It's randomly generated , And ours @keyframes yes transform It's dead . We can't dynamically generate one for each red envelope @keyframes, So it needs to be transformed HTML structure .

Here, a more ingenious method is used directly . whereabouts down Of @keyframes It's dead , rotate rotate Of @keyframes It needs to be dynamic . Then we'll put a parent element on the outer layer of the red envelope element , The parent element is responsible for the falling animation , The red envelope element is responsible for executing the rotation animation inside . namely :

  1. Parent element Settings width, height, left, id Properties of , Write the whereabouts of the dead down@keyframes
  2. Red envelope element width, height Inherit directly from the parent element . Dynamically set the of red envelope elements transform: rotate(); value , Then write dead transition The transition time is the falling time .
<script setup lang="ts">
function createRedPacket(xPonit: number) {
  const id = Math.random() * 1e18
  redPacketList.value.push(id)

  const { width, height, imageUrl } = props

  const packetWrapperEl = document.createElement('div')
  packetWrapperEl.classList.add('red-packet-img-wrapper')
  packetWrapperEl.id = 'packet-' + id
  packetWrapperEl.style.width = width + 'px'
  packetWrapperEl.style.height = height + 'px'
  packetWrapperEl.style.left = xPonit + 'px'

  const packetEl = document.createElement('img')
  packetEl.src = new URL(`../assets/${imageUrl}`, import.meta.url).href
  packetEl.classList.add('red-packet-img')

  setTimeout(() => {
    packetEl.style.transform = `rotate(${Math.random() > 0.5 ? 30 : -30}deg)`
  }, 100);
  packetEl.onclick = () => false
  packetWrapperEl.appendChild(packetEl)
  gameContainerRef.value!.appendChild(packetWrapperEl)
}
</script>

<style lang="scss">
.game-container {
  width: 100vw;
  height: 100vh;
  max-width: 750px;
  margin: 0 auto;
  overflow: hidden;
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);

  .red-packet-img-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(-100%);
    animation: down 3s linear forwards;
    font-size: 0;
    background-color: #39c5bb;
  }

  .red-packet-img {
    width: 100%;
    height: 100%;
    position: relative;
    transition: all 3s ease;
    transform: rotate(0);
  }

  @keyframes down {
    0% {
      transform: translateY(-100%);
    }

    100% {
      transform: translateY(110vh);
    }
  }
}
</style>
 Copy code 

Add a background to the parent element , Better understand

12

Red envelope click event judgment

Because the red envelopes are one by one div, Click events can be written very naturally . In order to facilitate the calculation after autumn , We put the red envelope of each click id All saved to hitedPacketIdList in , By the way, add the anti shake function .

const hitedPacketIdList = ref<number[]>([])

function hitPacket(id: number) {
  if (!hitedPacketIdList.value.includes(id)) {
    hitedPacketIdList.value.push(id)
    const hitEl = document.querySelector(`#packet-${id}`)

    if (gameContainerRef.value && hitEl) {
      gameContainerRef.value.removeChild(hitEl)
    }
  }
}

function createRedPacket(xPonit: number) {
	// ...
    packetEl.onclick = () => hitPacket(id)
    // ...
}
 Copy code 
13

The location and whereabouts of the generated red envelope

Nothing special , Copy and paste directly

<script setup lang="ts">
onMounted(async () => {
  const pointList = computedXPoint()
  const { split, totalPackets } = props
  const allPointList = [...Array(Math.ceil(totalPackets / split))].map(() => getRandomArray(pointList)).flat().splice(0, totalPackets).map(Math.ceil)

  let index = 0
  let timer: number | null = null
  createRedPacket(allPointList[index++])
  timer = setInterval(() => {
    if (index >= totalPackets - 1) {
      clearInterval(timer!)
    }
    createRedPacket(allPointList[index++])
  }, 700)
})
</script>
 Copy code 
15

Yes, of course , Red envelope elements that leave the scope of the view . We clean it regularly every second . such DOM The node will only store a small amount of the visible area DOM, Improved performance .

16

Deduction details ( Improve user experience )

Red envelope Click to expand the hot area

Eyes can't keep up with fingers . The red envelope keeps falling , It is possible that when clicking on the screen , The red envelope has dropped below the click position , Cause the game to judge that you didn't hit the red envelope . In order to facilitate the reaction of older players like me , We need to make the red envelope have a larger hot area of up and down clicks .

The implementation is also very simple , The height of the parent element increases , Red envelope elements can be added with the same upper and lower inner margins . Add a background to the red envelope to see the effect ( Green is the parent element , Blue is the red envelope, click the hot area ).

<script setup lang="ts">
function createRedPacket(xPonit: number) {
  // ...
  packetWrapperEl.style.height = height + padding * 2 + 'px'
  // ...
  packetEl.style.padding = `${padding}px 0`
  // ...
} 
</script>

<style lang="scss">
.game-container {
  // ...
  .red-packet-img {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    position: relative;
    transition: all 3s ease;
    transform: rotate(0);
    background-color: #66ccff;
  }
  // ...
}
</style>
 Copy code 
14

The size of the red envelope is adaptive

In order to avoid excessive red envelopes on small screen devices , Or the red envelope on the large screen device is too small . The adaptation of red envelope size naturally needs to be arranged . Not much said , Know everything. Adaptive layout scheme .

function getPx2VWSize(pixel: number) {
  const maxWindowInnerWidth = window.innerWidth 
  const design1px2vw = 1 / (750 / 100)
  const current1px2vw = pixel * design1px2vw * (maxWindowInnerWidth / 100)
  return current1px2vw
}
 Copy code 

Methods with the , Then give it to all width、height、padding Just set a function . There are only two functions involved createRedPacket、computedXPoint.

image-20220429003117466image-20220429003133660

PC End fit

But users are PC When you open the game , We obviously don't want users to see this strange thing .

image-20220429003506920

therefore , We need to Limit the maximum width of the game container , And Center

.game-container {
  width: 100vw;
  height: 100vh;
  max-width: 750px;
  margin: 0 auto;
  overflow: hidden;
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  // ...
}
 Copy code 

The width is the largest , The adaptive method naturally needs to be adapted

function computedXPoint() {
  const { split, screenPadding, width } = props
  const maxScreenWidth = (window.innerWidth > 750 ? 750 : window.innerWidth) - screenPadding * 2
  const maxFreeSpace = maxScreenWidth - getPx2VWSize(width) * split
  const marginSpace = maxFreeSpace / (split + 1)
  return Object.keys([...Array(split)]).map(Number).map(x => screenPadding + marginSpace + (getPx2VWSize(width) + marginSpace) * x)
}

function getPx2VWSize(pixel: number) {
  const maxWindowInnerWidth = window.innerWidth > 750 ? 750 : window.innerWidth
  const design1px2vw = 1 / (750 / 100)
  const current1px2vw = pixel * design1px2vw * (maxWindowInnerWidth / 100)
  return current1px2vw
}
 Copy code 
image-20220429004047426

Size solves , But when you click on the red envelope , I accidentally shook my hand . Click on the red envelope picture to turn into a long press and drag picture , This symptom will appear .

19

The symptoms of hand shaking are easy to treat , Let's just disable the drag and drop event of the picture (ಥ _ ಥ)

function createRedPacket(xPonit: number) {
  // ...
  packetEl.ondragstart = () => false
  // ...
}
 Copy code 

Click feedback

Click on the red envelope , None of them +1 Isn't your feedback unreasonable . The implementation is also very simple , Click to hit the red envelope . Add another... In the center of the red envelope +1 picture , Add some animation ,800ms after *+1 picture * remove ( disappear ) that will do .

<script setup lang="ts">
function hitPacket(id: number) {
  if (!hitedPacketIdList.value.includes(id)) {
    hitedPacketIdList.value.push(id)
    const hitEl = document.querySelector(`#packet-${id}`)

    if (gameContainerRef.value && hitEl) {
      const { top, left, width, height } = hitEl.getBoundingClientRect()
      gameContainerRef.value.removeChild(hitEl)
      const topPoint = top + height / 2
      const lefPoint = left + width / 2
      const hitImageEl = document.createElement('img')
      hitImageEl.classList.add('hit-animation')
      hitImageEl.src = new URL(`../assets/add-done.png`, import.meta.url).href
      hitImageEl.style.position = 'absolute'
      hitImageEl.style.top = topPoint + 'px'
      hitImageEl.style.left = lefPoint + 'px'
      hitImageEl.style.width = getPx2VWSize(96) + 'px'
      hitImageEl.style.height = getPx2VWSize(50) + 'px'
      document.body.appendChild(hitImageEl)
      setTimeout(() => {
        document.body.removeChild(hitImageEl)
      }, 800)
    }
  }
}
</script>

<style>
.hit-animation {
  animation: rise 0.7s ease forwards;
}

@keyframes rise {
  0% {
    transform: translateY(0);
    opacity: 1;
  }

  100% {
    transform: translateY(-100%);
    opacity: 0;
  }
}
</style>
 Copy code 
21

Load pictures in advance

When the user network is not good , It's very likely that the game has begun , But the red envelope picture hasn't been loaded yet , If users can't see the red envelope, they won't know what to do . When the red envelope is loaded , The red envelopes may have been destroyed several times , Extremely affected our Coupon delivery plan . So before we start the game , You need to load all the pictures involved in the game at one time in advance .

async function loadAllImage() {
  const loading = Loading.service({ lock: true, fullscreen: false, background: 'transparent' })
  await Promise.all([
    loadImages(new URL('../assets/game-coupon.png', import.meta.url).href),
    loadImages(new URL('../assets/game-red-packet.png', import.meta.url).href),
    loadImages(new URL('../assets/add-done.png', import.meta.url).href),
  ])
  loading.close()
}
 Copy code 

Optimize red envelope click events

For now , Every time you create a red envelope , We will bind a click event to each red envelope . For a person who has pursuit , These capabilities should not be wasted . We can make full use of Event delegation , Just bind an event to the game container , You can achieve click feedback of all red envelope hits .

<script>
onMounted(async () => {
  if (gameContainerRef.value) {
    gameContainerRef.value.addEventListener('click', (e) => {
      const hitClassName = (e.target as HTMLElement).className
      if (hitClassName === 'red-packet-img') {
        const hitEl = (e as any).path[1]
        const id = hitEl.id
        
        if (!hitedPacketIdList.value.includes(id)) {
          hitedPacketIdList.value.push(id)
          const { top, left, width, height } = hitEl.getBoundingClientRect()
          gameContainerRef.value!.removeChild(hitEl)
          const topPoint = top + height / 2
          const lefPoint = left + width / 2
          const hitImageEl = document.createElement('img')
          hitImageEl.classList.add('hit-animation')
          hitImageEl.src = new URL(`../assets/add-done.png`, import.meta.url).href
          hitImageEl.style.position = 'absolute'
          hitImageEl.style.top = topPoint + 'px'
          hitImageEl.style.left = lefPoint + 'px'
          hitImageEl.style.width = getPx2VWSize(96) + 'px'
          hitImageEl.style.height = getPx2VWSize(50) + 'px'
          document.body.appendChild(hitImageEl)
          setTimeout(() => {
            document.body.removeChild(hitImageEl)
          }, 800)
        }
      }
    })
  }
})
</script>
</script>
 Copy code 

Specific performance

CSS The final performance is divided into the following four steps :Recalculate Style -> Layout -> Paint Setup and Paint -> Composite Layers.

image-20220428020956325

ransform It's located in Composite Layers layer , Browsers will also target transform Turn on GPU Speed up . Use css3 Hardware acceleration , Let the animation not cause reflow redrawing .

image-20220428021351130

Each red envelope has its own independent synthetic layer , We turn on Layouts Just look at the layer .

20

We might as well generate more red envelopes and try . The result is also , The game runs almost full frames .

17

In what I wrote canvas In the method , The same parameters , Although the gap is not very big , But it still can't be full , Sure enough, the hardware acceleration is a little stronger .

22

To sum up

Looking back, I thought , The demand for red envelope rain is not as complex as expected . The first thing to consider a large number of DOM Nodes can lead to vertical performance degradation problem , It seems that there is no need to consider . Because of the red envelope rain DOM node , There will only be the number of single digit red envelopes you see on the screen . Red envelopes out of sight DOM Nodes have long been removed .

So , Simple animation requirements , use CSS3 It must be true ,GPU Accelerated performance is no joke .

As for complex functions , Handwriting canvas Don't reality . yes cocos It doesn't smell good ? Why handwriting canvas game .

Reference material

  1. developers.google.cn - Google Rendering performance related content provided
  2. 【 translation 】HTML5 Canvas Click area detection and how to monitor Canvas Click events of various graphics on
  3. How to create a highly available full screen red envelope rain

copyright notice
author[Mirai white],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/04/202204292019291248.html

Random recommended