【盯着鼠标的写轮眼】花里胡哨的CSS技巧+1

3,154 阅读4分钟

前言

第一次发表文章,确实令人激动,给大家分享点在闲(mo)暇(yu)时间写的简单的CSS小玩意儿,没什么特别的技术含量,有兴趣的可以自己拿去玩玩。如果有什么没写好的地方,大家多多包涵,欢迎提出指正!!

由于我本身是个火影迷,其中对宇智波的写轮眼更加情有独钟,觉得老炫酷了,可能是因为男人至死是少年,中二病永存心中吧!我有个宏伟的梦想,就是希望现实世界真的有写轮眼,自己也能拥有一双独特的无线万花筒和六勾玉轮回眼啊!

当然现实世界除了带个虚假的美瞳,是不存在写轮眼的,那么就用CSS来自我安慰一下吧!

在这里我一共包括7个火影里主要的写轮眼(三勾玉(一二勾玉去掉两个勾玉即可),鼬神,带土/卡卡西,二柱子,止水,泉奈,马达拉)和1个六勾玉轮回眼(普通轮回眼只是把勾玉去掉,所以就没单独画了,九勾玉轮回眼懒得画所以没画哈哈哈哈哈),另外有12个我自己瞎画的万花筒。

(为了方便理解,同时有些单词实在不好找英文单词来翻译,所以CSS的类名和ID基本都是统一用的拼音)


效果展示


组成

眼睛从外到内基本组成包含了:

脸颊、眼睛、眼帘、眼球、眼珠、眼珠内阴影、虹膜、瞳孔、花纹、花纹元素

当然花纹比较特殊的眼睛,也会包含一些特殊的组成部分,具体可以往下看

为了更加贴近现实的眼睛,在眼珠的设计中,我还添加了虹膜这个部分,虹膜是用js用for循环生成的,长度在规定范围内随机

    const hongmo = document.getElementsByClassName("hongmo_lines");
    for (let h = 0; h < hongmo.length; h++) {
      const hongmoItem = hongmo[h];
      for (let i = 0; i < 100; i++) {
        const hongmo_line = document.createElement("div");
        const height = (Math.random() + 1) * 26;
        hongmo_line.style.transform = `rotate(${((i + 1) * 360) / 100}deg)`;
        hongmo_line.style.height = `${height}px`;
        hongmo_line.style.top = `calc(50% + ${0}px)`;
        hongmoItem.appendChild(hongmo_line);
      }
    }

勾玉

勾玉的基本构成包括了两部分,一个是小圆点,一个是伪元素实现的勾玉的小尾巴

    .circle {
      position: absolute;
      width: 12px;
      height: 12px;
      border-radius: 12px;
      left: 0;
      top: 0;
      background-color: #000;
    }
    .circle::before {
      content: "";
      position: absolute;
      width: 6px;
      top: 6px;
      left: 0px;
      height: 12px;
      border-color: currentColor;
      border-style: solid;
      border-width: 0 0 0 6px;
      border-radius: 0 0 0 100%;
    }

万花筒图案

关于各种万花筒的图案,特别是原创部分,大多是基于这些css属性就是

//具体参数根据需要自行调整
      border-color: currentColor;
      border-style: solid;
      border-width: 0 0 0 6px;
      border-radius: 0 0 0 100%;

据此,得出的团再通过transform、绝对坐标等,三个或四个互相组合,就能得到更多有趣的万花筒图案了!

轮回眼

轮回眼其实比所有万花筒都简单,要调的参数也少很多,可以理解为就是几个圈圈,加几个勾玉调调位置就成了

右眼

为了避免有太多一样的代码,右眼我就直接用js拷贝,然后镜像反转了

    const lianjia = document.getElementsByClassName("lianjia");
    for (let i = 0; i < lianjia.length; i++) {
      const rightEye = lianjia[i]
        .getElementsByClassName("yanjing")[0]
        .cloneNode(true);
      rightEye.style.transform = "rotateY(180deg)";
      rightEye.getElementsByClassName("yanqiu")[0].style.transform =
        "translate(-50%, -50%) rotateY(180deg)";
      lianjia[i].appendChild(rightEye);
    }

[更新] 眼球效果优化

为了突出眼睛,背景设置成了灰色(因为黑色看不到睫毛,所以调成了灰色),眼白加了内阴影,另外加了两个椭圆反光点,采用border,border-radius,filter:blur实现,这样看起来眼睛会更有立体感

[更新] 新增了眼睛聚焦跟随鼠标的效果

注:暂时还没有实现轮回眼的随动效果

已更新轮回眼随动效果

思路:

添加一个MouseMove的EventListener,并把每一个眼球都执行以下换算:

  • 眼珠位置:根据鼠标的与视口的相对位置,以等比缩放到眼球的left和top中,即可得到上下左右移动的眼球相对位置

  • 瞳孔大小:根据两个眼球的瞳孔,与鼠标的距离,采用两者的最小值作为最终距离,以该距离除以一个最大距离的阈值,得到一个缩放比例,该比例乘以瞳孔最大可伸缩直径(这里是20),再加上瞳孔最小直径(这里是10),即可得到动态的瞳孔尺寸值

[更新] 添加眨眼

眼眶由border绘制更换为box-shadow绘制,添加了眼帘,并设定随机5-8秒自动眨眼,以模拟真实的眼睛

[更新] 添加点击释放幻术效果

添加点击释放幻术的效果,点击后会先眨眼,然后有一个透明的写轮眼图案逐渐旋转放大,模拟火影动画里用写轮眼释放幻术或者瞳术的情景

动效相关的JS代码


    const scale = 0.3; // 缩放大小,值等同 style -> .lianjia -> transform: scale(0.3)
    const lianjia = document.getElementsByClassName("lianjia");
    // 生成右眼
    for (let i = 0; i < lianjia.length; i++) {
      const rightEye = lianjia[i]
        .getElementsByClassName("yanjing")[0]
        .cloneNode(true);
      rightEye.style.transform = "rotateY(180deg)";
      rightEye.getElementsByClassName("yanqiu")[0].style.transform =
        "translate(-50%, -50%) rotateY(180deg)";
      lianjia[i].appendChild(rightEye);
    }
    // 定时眨眼
    const blink = (face, cb = () => {}) => {
      const eyes = face.getElementsByClassName("yanjing");
      const yanlians = face.getElementsByClassName("yanlian");
      yanlians[0].style.height = "100%";
      yanlians[2].style.height = "100%";
      for (let i = 0; i < eyes.length; i++) {
        // eyes[i].style.borderWidth = "150px 0px 40px 14px";
        // eyes[i].style.boxShadow =
        //   "-2px -2px 16px 2px inset, -18px -20px 0px 13px black";
        eyes[i].style.height = "120px";
      }
      setTimeout(() => {
        yanlians[0].style.height = 0;
        yanlians[2].style.height = 0;
        for (let i = 0; i < eyes.length; i++) {
          // eyes[i].style.borderWidth = "30px 0px 0px 14px";
          // eyes[i].style.boxShadow =
          //   "-2px -2px 16px 2px inset, -18px -20px 0px 13px black";
          eyes[i].style.height = "200px";
        }
        setTimeout(() => {
          cb();
        }, 200);
      }, 300);
    };
    const randomBlink = (face = document.createElement("div")) => {
      blink(face);
      setTimeout(() => {
        randomBlink(face);
      }, 5000 + Math.random() * 3000);
    };
    for (let i = 0; i < lianjia.length; i++) {
      randomBlink(lianjia[i]);
    }
    // 点击释放幻术
    let showingMagic = false;
    const magic = (face = document.createElement("div")) => {
      showingMagic = true;
      const xuanzhuanBoxes = face.getElementsByClassName("xuanzhuanBox");
      for (let i = 0; i < xuanzhuanBoxes.length; i++) {
        xuanzhuanBoxes[i].style.animation = "none";
        setTimeout(() => {
          xuanzhuanBoxes[i].style.animation = "rotate 0.8s ease-out 1";
        });
      }
      const eyeBall = face.getElementsByClassName("yanqiu");
      for (let i = 0; i < eyeBall.length; i++) {
        const yanzhu = eyeBall[i].getElementsByClassName("yanzhu")[0];
        const ball = eyeBall[i].cloneNode(true);
        const ballRect = yanzhu.getBoundingClientRect();
        ball.style.width = `fit-content`;
        ball.style.height = `fit-content`;
        ball.style.position = "fixed";
        ball.style.left = `${ballRect.left}px`;
        ball.style.top = `${ballRect.top}px`;
        ball.style.zIndex = "10";
        ball.style.transform = `translate(-${50 * (1 - scale)}%, -${
          50 * (1 - scale)
        }%) scale(${scale}) rotate(0)`;
        ball.style.opacity = "0.1";
        ball.style.filter = "blur(0px)";
        // ball.style.animation = "magic 2s ease-out 1";
        ball.style.transition = "1.8s all ease-out";
        setTimeout(() => {
          ball.style.transform = `translate(-${50 * (1 - scale)}%, -${
            50 * (1 - scale)
          }%) scale(${scale * 8}) rotate(360deg)`;
          ball.style.opacity = "0";
          ball.style.filter = "blur(2px)";
        }, 0);
        document.body.appendChild(ball);
        setTimeout(() => {
          document.body.removeChild(ball);
          showingMagic = false;
        }, 2000);
      }
    };
    const showMagic = (e, face = document.createElement("div"), index) => {
      if (!showingMagic) blink(face, () => magic(face));
    };
    window.addEventListener("click", (e) => {
      for (let i = 0; i < lianjia.length; i++) {
        showMagic(e, lianjia[i], i);
      }
    });
    // 生成虹膜
    const hongmo = document.getElementsByClassName("hongmo_lines");
    for (let h = 0; h < hongmo.length; h++) {
      const hongmoItem = hongmo[h];
      for (let i = 0; i < 100; i++) {
        const hongmo_line = document.createElement("div");
        const height = (Math.random() + 1) * 26;
        hongmo_line.style.transform = `rotate(${((i + 1) * 360) / 100}deg)`;
        hongmo_line.style.height = `${height}px`;
        hongmo_line.style.top = `calc(50% + ${0}px)`;
        hongmoItem.appendChild(hongmo_line);
      }
    }
    // 眼球追踪
    let movingBall = Array.from(lianjia).map(() => false);
    const moveBall = (e, face = document.createElement("div"), index) => {
      if (movingBall[index]) return;
      movingBall[index] = true;
      setTimeout(() => {
        movingBall[index] = false;
      }, 60);
      const eyes = face.getElementsByClassName("yanjing");
      // ---眼球---
      const eyeWidth = eyes[0].offsetWidth * scale;
      const leftEye = eyes[0];
      const rightEye = eyes[1];
      const leftEyeRect = eyes[0].getBoundingClientRect();
      const rightEyeRect = eyes[1].getBoundingClientRect();
      const eyesSpacing = rightEyeRect.x - leftEyeRect.x - eyeWidth - 20;
      // ---眼珠子---
      // 右眼是左眼的镜像,因此left和right会反转
      // ---左右移动---
      const leftEyeBall = leftEye.getElementsByClassName("yanqiu")[0];
      const rightEyeBall = rightEye.getElementsByClassName("yanqiu")[0];
      const LeftEyeBall_MaxX = 280;
      const LeftEyeBall_MinX = 120;
      const LeftEyeBall_Center = 200;
      const RightEyeBall_MinX = 120;
      const RightEyeBall_MaxX = 280;
      const RightEyeBall_Center = 210;
      const seeLeft = leftEyeRect.x + eyeWidth;
      const seeRight = seeLeft + eyesSpacing;
      const seeCenter = seeLeft + eyesSpacing / 2;
      if (e.x < seeLeft) {
        const moveRatio = e.x / seeLeft;
        // console.log(moveRatio)
        leftEyeBall.style.left = `${
          LeftEyeBall_MinX + moveRatio * (LeftEyeBall_Center - LeftEyeBall_MinX)
        }px`;
        rightEyeBall.style.left = `${
          RightEyeBall_MaxX +
          moveRatio * (RightEyeBall_Center - RightEyeBall_MaxX)
        }px`;
      } else if (e.x > seeRight) {
        const moveRatio =
          1 - (window.innerWidth - e.x) / (window.innerWidth - seeRight);
        // console.log(moveRatio)
        leftEyeBall.style.left = `${
          LeftEyeBall_Center +
          moveRatio * (LeftEyeBall_MaxX - LeftEyeBall_Center)
        }px`;
        rightEyeBall.style.left = `${
          RightEyeBall_Center +
          moveRatio * (RightEyeBall_MinX - RightEyeBall_Center)
        }px`;
      } else {
        const moveRatio = 1 - Math.abs(e.x - seeCenter) / eyesSpacing;
        // console.log(moveRatio)
        leftEyeBall.style.left = `${
          LeftEyeBall_Center +
          moveRatio * (LeftEyeBall_MaxX - LeftEyeBall_Center)
        }px`;
        rightEyeBall.style.left = `${
          RightEyeBall_MaxX +
          (1 - moveRatio) * (RightEyeBall_Center - RightEyeBall_MaxX)
        }px`;
      }
      // 上下移动
      const minY = 8; // 向上看
      const maxY = 120; // 向下看
      const midY = 90; // 平视
      const eyeHeight = eyes[0].offsetHeight * scale;
      const eyeMiddle = leftEyeRect.y + eyeHeight / 2;
      if (e.y < eyeMiddle) {
        const moveRatio = e.y / eyeMiddle;
        // console.log(moveRatio)
        leftEyeBall.style.top = `${minY + moveRatio * (midY - minY)}px`;
        rightEyeBall.style.top = `${minY + moveRatio * (midY - minY)}px`;
      } else if (e.y > eyeMiddle) {
        const moveRatio =
          1 - (window.innerHeight - e.y) / (window.innerHeight - eyeMiddle);
        // console.log(moveRatio)
        leftEyeBall.style.top = `${midY + moveRatio * (maxY - midY)}px`;
        rightEyeBall.style.top = `${midY + moveRatio * (maxY - midY)}px`;
      }
      // 瞳孔
      const MinSize = 16;
      const MaxSize = 30;
      const MaxDist = Math.min(
        window.innerWidth,
        window.innerHeight,
        eyeWidth * 3
      ); // 控制瞳孔聚焦最远距离的阈值
      const LeftEyeBallRect = leftEyeBall.getBoundingClientRect();
      const RightEyeBallRect = rightEyeBall.getBoundingClientRect();
      const leftDistX = Math.abs(
        e.x - (LeftEyeBallRect.x + LeftEyeBallRect.width / 2)
      );
      const leftDistY = Math.abs(
        e.x - (LeftEyeBallRect.y + LeftEyeBallRect.height / 2)
      );
      const rightDistX = Math.abs(
        e.x - (RightEyeBallRect.x + RightEyeBallRect.width / 2)
      );
      const rightDistY = Math.abs(
        e.x - (RightEyeBallRect.y + RightEyeBallRect.height / 2)
      );
      const leftDist = Math.sqrt(
        Math.pow(leftDistX, 2),
        Math.pow(leftDistY, 2)
      );
      const rightDist = Math.sqrt(
        Math.pow(rightDistX, 2),
        Math.pow(rightDistY, 2)
      );
      const realDist = Math.min(leftDist, rightDist);
      const sizeRatio = 1 - realDist / MaxDist;
      // console.log("sizeRatio", sizeRatio)
      const tongkongs = face.getElementsByClassName("tongkong");
      for (let i = 0; i < tongkongs.length; i++) {
        tongkongs[i].style.width = `${MinSize + 20 * sizeRatio}px`;
        tongkongs[i].style.height = `${MinSize + 20 * sizeRatio}px`;
      }
    };
    document.addEventListener("mouseenter", (e) => {
      for (let i = 0; i < lianjia.length; i++) {
        moveBall(e, lianjia[i], i);
      }
    });
    document.addEventListener("mousemove", (e) => {
      for (let i = 0; i < lianjia.length; i++) {
        moveBall(e, lianjia[i], i);
      }
    });
    // moveBall();
  

[更新] 迁移使用

  • 代码复制 - CV的功夫,就不详述了,代码也没有很复杂

  • 调节大小 - 调节大小时不要在css里面调宽高,因为里面的各个组成部分的长宽等等都有各自固定的值,如果要改变大小,则可以对 .lianjia 的 transform: scale(0.3) 的值进行变更,同时对JS代码内的scale值变更为与.lianjia的transform相同的值即可


总结

这些眼睛大部分都是采用了border、border-radius、clip-path去做的,起因是当时在搜别的CSS样式时,见到了相关的内容,脑袋灵光一闪,当下也有足够的时间(moyu),于是说干就干!就完成了目前的静态内容。

不过有些形状多少还是差了点东西,比如佐助的五芒星,其实不是纯粹的椭圆,尖端是稍微带点尖的,再次希望各位大佬多多包涵,如果有想法,也希望各位大佬多多指正。

等之后还有时间,一定把动效也给补上!让咱的写轮眼轮回眼更逼真更有神韵!

(转载请注明来源)