Skip to content

背景随图片变化 以及 loading

展示

请将鼠标移入当前图片,背景颜色将会跟随图片主色调展示

点击图片将会更换图片
点击展开
vue
<template>
  <blockquote>点击图片将会更换图片</blockquote>
  <el-button @click="handleClick" class="cor-tip button">点击更换图片</el-button>
  <div class="container">
    <div class="grid" :style="style">
      <div class="item" v-for="(url, i) in images">
        <img crossorigin="anonymous" @mouseenter="handleMouseEnter($event, i)" @mouseleave="handleMouseLeave" :src="url"
          @click="handleClick" :style="{
    opacity: hoverIndex === -1 ? 1 : i === hoverIndex ? 1 : 0.2,
  }" />
      </div>
      <transition name="fade">
        <div class="mask" v-show="loading">
          <div class="dot" v-for="item in 36" :key="item"></div>
        </div>
      </transition>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed, reactive, onMounted, nextTick } from "vue";
import quantize from "quantize";
import type { RgbPixel } from "quantize";
import { getRandomNum } from "docs/utils/index.ts";
const address = "https://picsum.photos/200/200";
const images: string[] = reactive([]);
const loading = ref(true);
const hoverIndex = ref(-1);
const color1 = ref("transparent");
const color2 = ref("transparent");
const color3 = ref("transparent");
const useQuantize = (pixels: number[], k: number = 5) => {
  // 存储颜色的数组
  const colors: RgbPixel[] = [];
  // 遍历每个像素
  for (let i = 0; i < pixels.length; i += 4) {
    // 提取像素的RGB值
    const r = pixels[i];
    const g = pixels[i + 1];
    const b = pixels[i + 2];
    const a = pixels[i + 3];
    // 取出不透明以及透明属性在
    if (typeof a === "undefined" || a >= 128) {
      if (!(r > 250 && g > 250 && b > 250)) {
        colors.push([r, g, b]);
      }
    }
  }
  const camp = quantize(colors, k);
  return camp ? camp.palette() : [];
};
const getMainColor = (img: HTMLImageElement) => {
  const canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  const ctx: any = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  canvas.innerHTML = "";
  return useQuantize(imageData.data, 3);
};
const handleMouseEnter = async (event: MouseEvent, index: number) => {
  hoverIndex.value = index;
  const mainColor = getMainColor(event.target as HTMLImageElement);
  const [c1, c2, c3] = mainColor.map(
    (c: number[]) => `rgb(${c[0]},${c[1]},${c[2]})`
  );
  color1.value = c1;
  color2.value = c2;
  color3.value = c3;
};
const handleClick = () => {
  getImage();
};
const handleMouseLeave = () => {
  hoverIndex.value = -1;
  color1.value = "transparent";
  color2.value = "transparent";
  color3.value = "transparent";
};
const style = computed(() => {
  return {
    "--c1": color1.value,
    "--c2": color2.value,
    "--c3": color3.value,
  };
});
onMounted(() => {
  getImage();
});
const getImage = () => {
  loading.value = true;
  const arr = new Array(4).fill(0);
  Promise.all(
    arr.map((_) => fetch(`${address}?r=${getRandomNum(0, 100)}`))
  ).then((res) => {
    images.splice(0, 4);
    Promise.all(res.map((r) => r.blob())).then((abc) => {
      images.push(...abc.map((r) => URL.createObjectURL(r)));
      nextTick(() => {
        loading.value = false;
      });
    });
  });
};
</script>

<style lang="scss" scoped>
.container {
  $containerSize: 640px; // 容器尺寸
  $paddingSize: 80px;

  @media (max-width: 768px) {
    $containerSize: 320px;
    $paddingSize: 20px;
  }

  position: relative;
  width: $containerSize;
  height: $containerSize;
  margin-top: 20px;
  overflow: hidden;

  .grid {
    width: $containerSize;
    padding: $paddingSize;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(2, 1fr);
    grid-gap: $paddingSize;
    background: linear-gradient(-61.8deg,
        var(--c1) 0%,
        var(--c2) 20%,
        var(--c3) 40%,
        var(--c1) 60%,
        var(--c2) 80%,
        var(--c3) 100%);
    animation: spread 0.5s ease-in forwards;
    transition-property: --c1, --c2, --c3;
    transition-duration: 0.5s;
    transition-timing-function: ease-in;

    .item {
      width: 100%;
      height: 100%;
      overflow: hidden;
      transition: 0.5s;
      border-radius: 5px;
      border: 0px solid #fff;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        transition: 0.5s;
      }

      &:hover {
        filter: drop-shadow(2px 2px 10px rgba(0, 0, 0, 0.5));
        border-width: 2px;
        transform: scale(1.1);
      }
    }
  }

  .mask {
    $loadingSize: 60px; // 加载中的尺寸
    $ballSize: 8px; // 小球尺寸
    $n: 36;
    $stepDeg: calc(360deg / $n);
    $ani-duration: 2000ms;
    position: absolute;
    $multiple: 1.4;
    $maskSize: $containerSize * $multiple;
    width: $maskSize;
    height: $maskSize;
    top: calc($containerSize / 2) - calc($maskSize / 2);
    left: calc($containerSize / 2) - calc($maskSize / 2);
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 2;
    border-radius: 50%;

    .dot {
      position: absolute;
      left: 50%;
      top: 50%;
      width: $ballSize;
      height: $ballSize;
      margin-left: calc(-1 * $ballSize / 2);
      margin-top: calc(-1 * $ballSize / 2);
      perspective: 70px;
      transform-style: preserve-3d;

      &::before,
      &::after {
        content: "";
        position: absolute;
        width: 100%;
        height: 100%;
        border-radius: 50%;
      }

      &::before {
        background: #7c6abb;
        top: -100%;
        animation: moveBlack $ani-duration infinite;
      }

      &::after {
        background: #55ad6f;
        top: 100%;
        animation: moveWhite $ani-duration infinite;
      }

      @mixin setBallDiff($i) {
        @if $i < ($n + 1) {
          &:nth-child(#{$i}) {
            transform: rotate($stepDeg * $i) translateY(-$loadingSize);

            &::before,
            &::after {
              animation-delay: calc(-1 * $ani-duration / $n) * 6 * $i;
            }
          }

          @include setBallDiff($i + 1);
        }
      }

      @include setBallDiff(1);
    }

    @keyframes moveWhite {
      0% {
        animation-timing-function: ease-in;
      }

      25% {
        transform: translate3d(0, -100%, -$ballSize);
        animation-timing-function: ease-out;
      }

      50% {
        transform: translate3d(0, -200%, 0);
        animation-timing-function: ease-in;
      }

      75% {
        transform: translate3d(0, -100%, $ballSize);
        animation-timing-function: ease-out;
      }
    }

    @keyframes moveBlack {
      0% {
        animation-timing-function: ease-in;
      }

      25% {
        transform: translate3d(0, 100%, $ballSize);
        animation-timing-function: ease-out;
      }

      50% {
        transform: translate3d(0, 200%, 0);
        animation-timing-function: ease-in;
      }

      75% {
        transform: translate3d(0, 100%, -$ballSize);
        animation-timing-function: ease-out;
      }
    }
  }

  .fade-enter-active,
  .fade-leave-active {
    transition: all 0.8s;
  }

  .fade-enter-from,
  .fade-leave-to {
    opacity: 0;
    transform: rotate(60deg);
  }
}
</style>

图片调色盘 视频讲解

黑白小球交替的loading效果 视频讲解