【地狱难度】面试官:你能够用纯 CSS 判断鼠标进入的方向吗?

升级版 CSS 判断鼠标进入方向

正直的勇者们经历远航,一路横扫除魔,终于来到了魔王(指面试官)所在的石塔。勇者们在石塔前的守夜人陈大鱼头(此处@陈大鱼头)那里接受了一个挑战——用纯 CSS 难度判断鼠标进入盒子的方向。html

陈大鱼头:“勇者留步,进入石塔前,请先使用这个挑战练练手吧!”git

在给定初始 HTML 结构下,编写代码,完成下图功能:github

勇者 A:“害,这还不简单...”面试

勇者 A 给四个盒子分别定位到上下左右四个方向,并利用其 Hover 状态制做动画,三下五除二完成了挑战浏览器

勇者们解决了难题,正痛快着。通过一路试炼(指金三银四中各类轰炸),彷佛没有什么能阻挡他讨伐魔王的步伐。他们满怀自信步入石塔内部。sass

而后,傻眼了。ruby

魔王:“勇者们,没有解决这个难题前,我金身!”。(指勇者们不得不屈服于面试官百般刁难的规则。)less

魔王微微一笑,道:“HTML + CSS,我也不给初始代码了,你想写啥都行,反正实现下面这玩意儿吧”。函数


图片效果

这是一条手动分割线,如下是解析。post


解析

主要思路就是利用选择器将和鼠标产生互动的盒子选择到咱们想要触发效果的盒子上,附加动画属性。

不过,通过一番搜索以后,发现现实与理想仍是有点差距的。

CSS 没有父选择器

我百度出来各类说法,说是父选择器的性能较差,没有浏览器厂商愿意作,因此父级选择器相关标准被推迟了。

不过我相信用一些比较奇怪(♂)的方法,能够伪造一个“父级选择器”出来。

首先想到的是,也许能够某种表单相关的盒子,再结合 CSS 属性选择器,达到选中对应元素的效果,就像是 :checked 选择器那样。结果一番操做后发现,没有能达到要求的这种东西。

表单 Form 元素却是能够经过当子组件处于 :focus 状态时,标记其自身为 :focus-within 状态。不过有个问题,它不能告诉咱们究竟是哪一个子组件处于 :focus ,这样的话咱们就不知道上下左右究竟是哪一个盒子和鼠标发生了互动。

又通过一番思索及尝试后,我选择了通用兄弟选择器(这里不介绍选择器具体做用了,不知道的同窗直接点连接去看 MDN)。下面直接讲代码结构。

HTML 以下:

<div class="container">
    <!-- 咱们须要在此处插入一些盒子,与鼠标互动,而后经过 “~ .head .eye”通用兄弟选择器,就能选中眼睛了 -->
    <div class="head">
        <div class="face">
            <div class="mouth"></div>
            <div class="eye-group">
                <div class="eye eye-left"></div>
                <div class="eye eye-right"></div>
            </div>
        </div>
    </div>
</div>
复制代码

在守夜人陈大鱼头那里,勇者们使用了四个 .block_hoverer 类标签与鼠标交互。

魔王这里则须要多一些,以下图红色区域,每个矩形都是一个与鼠标交互的盒子:

这里我是用绝对定位将盒子“黏贴”到脸上,主要有三个要考虑的地方:

  • 如何将盒子定位到圆周上
  • 如何肯定盒子自身旋转角度
  • 如何安排盒子的宽高使得盒子彻底覆盖脸的四周

若是盒子自己不旋转的话,会出现这种诡异的状况:

这里使用一个简单的运算去旋转盒子:

// 与鼠标交互的盒子的个数
$part: 72;
// 每一个盒子间的夹角
$part-degree: 360 / $part;

@for $i from 1 through $part {
    // 旋转角度这里加了 90deg 的偏移是受到了盒子的定位的影响。
    // 这里能够忽略,咱们只要清楚原理是根据循环肯定盒子的旋转角度就行了。
    transform: rotate((90 + $i * $part-degree) + unquote('deg'));
}
复制代码

有关第三点“如何安排盒子的宽高使得盒子彻底覆盖脸的四周”,则是经验性的东西,各位本身手动调一下盒子宽高,很容易就能整出来。这里要额外说起一下第一点,由于 CSS(SCSS)中是没有正弦余弦函数的,因此不能使用正余弦函数去将盒子定位到圆周上。

CSS 中是没有正弦余弦函数的,真的么?

(→_→)

好吧,确实没有,但又是一个能够伪造出来的东西...

/** 三角函数相关 */
/** @see 代码来自 http://jimyuan.github.io/blog/2015/02/12/trigonometry-in-sass.html,感谢 */

@function fact($number) {
    $value: 1;
    @if $number>0 {
        @for $i from 1 through $number {
            $value: $value * $i;
        }
    }
    @return $value;
}

@function pow($number, $exp) {
    $value: 1;
    @if $exp>0 {
        @for $i from 1 through $exp {
            $value: $value * $number;
        }
    } @else if $exp < 0 {
        @for $i from 1 through -$exp {
            $value: $value / $number;
        }
    }
    @return $value;
}

@function rad($angle) {
    $unit: unit($angle);
    $unitless: $angle / ($angle * 0 + 1);
    @if $unit==deg {
        $unitless: $unitless / 180 * pi();
    }
    @return $unitless;
}

@function pi() {
    @return 3.14159265359;
}

@function sin($angle) {
    $sin: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 10 {
        $sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
    }
    @return $sin;
}

@function cos($angle) {
    $cos: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 10 {
        $cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
    }
    @return $cos;
}
复制代码

有了三角函数,再把循环安排上,就能够造出不少东西了:

// 脸的宽度
$face-width: 300;
// 与鼠标交互的盒子的个数
$part: 72;
// 每一个盒子间的夹角
$part-degree: 360 / $part;

@for $i from 1 through $part {

    /* 计算出盒子在圆周上的定位。须要注意的是,须要加上外围盒子的宽高及自身宽高对应的一些偏移量。 */
    $angle: ($i / $part) * 2 * 3.1416;
    $x: cos($angle) * $face-width / 2 + 500;
    $y: sin($angle) * $face-width / 2;

    // 熟悉的 :nth-child 选择器。个人上一篇博客已经说到过这玩意儿了,快去看!
    .box_hover:nth-child(#{$i}) {
        left: $x + unquote('px');
        top: $y + unquote('px');
        transform: rotate((90 + $i * $part-degree) + unquote('deg'));

        // 不一样的盒子的 :hover 状态,会改变其兄弟 .head 类盒子里的一些东西,这里涉及到眼睛是如何制做的,稍后会说。
        &:hover ~ .head {
            $ty: sin($angle) * $face-width / 50;
            $tx: cos($angle) * $face-width / 50;
            left: calc(50% - #{$tx}px);
            top: calc(50% - #{$ty}px);
            .eye {
                &:after {
                    background-position: 100% 50%;
                    transform: rotate(
                        (0 + $i * $part-degree) + unquote('deg')
                    );
                }
            }
        }
    }
}
复制代码

那么最后说起一下眼睛的制做。

最早想到的方案确定是用绝对定位,而后 hover 不一样的盒子时,给眼睛设置不一样的 left、top 值。

可是这个方案不可行,由于一旦给眼睛加上动画以后,一旦鼠标移动地很快,left、top 值的变化是直线,那么眼睛的行动轨迹就会很奇怪(♂)。

可是这个方案不可行,由于我不想写更多的数学了,我仍是用回了 rotate 这个迷人的小东西。

当没有 hover 任何盒子时,咱们给 .eye 类盒子的中心画一个圆(此时圆在盒子的中心):

当 hover 了某个盒子以后,咱们把 .eye 类盒子旋转一下,而且改变圆的位置(此时圆在盒子的右侧的中心):

这样的话就完成了。

不要脸求个点赞收藏分享啦。(  o=^•ェ•)o

完整的代码贴在下面,各位也能够去个人博客康康具体的实现(掘金我的主页边有个网站的小按钮,点那个能够直达暗示关注!)。

源码

<div class="container">
    <!-- 这行是 VueJS 语法,注意一下 -->
    <div class="circle" v-for="item in 72"></div>
    <div class="shadows"></div>
    <div class="head">
        <div class="face">
            <div class="mouth"></div>
            <div class="eye-group">
                <div class="eye eye-left"></div>
                <div class="eye eye-right"></div>
            </div>
        </div>
    </div>
</div>
复制代码
/** 三角函数 @see http://jimyuan.github.io/blog/2015/02/12/trigonometry-in-sass.html */

@function fact($number) {
    $value: 1;
    @if $number>0 {
        @for $i from 1 through $number {
            $value: $value * $i;
        }
    }
    @return $value;
}

@function pow($number, $exp) {
    $value: 1;
    @if $exp>0 {
        @for $i from 1 through $exp {
            $value: $value * $number;
        }
    } @else if $exp < 0 {
        @for $i from 1 through -$exp {
            $value: $value / $number;
        }
    }
    @return $value;
}

@function rad($angle) {
    $unit: unit($angle);
    $unitless: $angle / ($angle * 0 + 1);
    @if $unit==deg {
        $unitless: $unitless / 180 * pi();
    }
    @return $unitless;
}

@function pi() {
    @return 3.14159265359;
}

@function sin($angle) {
    $sin: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 10 {
        $sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
    }
    @return $sin;
}

@function cos($angle) {
    $cos: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 10 {
        $cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
    }
    @return $cos;
}

/*********************** 笑脸 */
/* 笑脸我是在 CodePen 里的某个项目基础上改的,地址忘了,汗 */

$container-height: 500;

.container {
    position: relative;
    width: 1000px;
    height: $container-height + unquote('px');
    overflow: hidden;
    background: #feee9d;
}
.container {
    * {
        position: absolute;
    }
    *:not(.circle):before,
    *:not(.circle):after {
        content: '';
        position: absolute;
    }

    $face-width: 300;
    $circle-width: $container-height;

    /** 监听器代码 */

    .circle {
        position: absolute;
        width: 30px;
        height: $circle-width + unquote('px');
        // &:hover {
        // background: red;
        // }
    }
    $part: 72;
    $part-degree: 360 / $part;
    @for $i from 1 through $part {
        $angle: ($i / $part) * 2 * 3.1416;
        $x: cos($angle) * $face-width / 2 - 5 + 500;
        $y: sin($angle) * $face-width / 2;
        .circle:nth-child(#{$i}) {
            left: $x + unquote('px');
            top: $y + unquote('px');
            transform: rotate((90 + $i * $part-degree) + unquote('deg'));
            &:hover ~ .head {
                $ty: sin($angle) * $face-width / 50;
                $tx: cos($angle) * $face-width / 50;
                left: calc(50% - #{$tx}px);
                top: calc(50% - #{$ty}px);
                .eye {
                    &:after {
                        background-position: 100% 50%;
                        transform: rotate(
                            (0 + $i * $part-degree) + unquote('deg')
                        );
                    }
                }
            }
        }
    }

    /** 样式代码 */

    .shadows,
    .head {
        border-radius: 50%;
        width: $face-width + unquote('px');
        height: $face-width + unquote('px');
        transform: translate(-50%, -50%);
        top: calc(50%);
        left: calc(50%);
        cursor: pointer;
    }
    .shadows {
        background-color: darken(#fbd671, 20%);
    }
    .head {
        background-color: #fbd671;
    }

    .face {
        width: 150px;
        height: 170px;
        top: 75px;
        left: 75px;
    }

    .mouth {
        width: 100%;
        height: 70px;
        bottom: 0;
        background-color: #20184e;
        border: 5px solid #20184e;
        border-radius: 150px 150px 10px 10px;
        overflow: hidden;
        &:after {
            background-color: #f15962;
            width: 100px;
            height: 60px;
            left: 20px;
            top: 40px;
            border-radius: 50%;
        }
    }

    .eye-group {
        top: 10px;
        width: 150px;
        height: 50px;
        .eye {
            width: 40px;
            height: 40px;
            background-color: #20184e;
            border-radius: 50%;
            border: 5px solid #20184e;
            &:after {
                width: 100%;
                height: 100%;
                top: 0;
                left: 0;
                background: radial-gradient(#fbd671 68%, #20184e 68%);
                background-size: 10px 10px;
                background-repeat: no-repeat;
                background-position: 50% 50%;
                transition: 0.1s;
            }
            &.eye-left {
                left: 15px;
            }
            &.eye-right {
                right: 15px;
            }
        }
    }
}
复制代码

后记

昨天我在陈大鱼头的文章下评论了“我可使用 10x10 的网格系统造一个精准度更高的玩意儿出来”,害,惨。

我本来的想法是想实现一下这样的东西,见下图片(纯 CSS):

效果图

解析

不过因为 CSS 没有父级选择器,这种实现几乎没什么用,看个人 HTML 结构就知道了。

<div class="container">
    <p class="info">盒子外部</p>
    <div class="boxes">
        <div class="box" v-for="item in 5">
            <div class="box-inner">
                <div class="left" />
                <div class="right" />
                <!-- .des 类是图中会变化的那些文字的容器。经过改变其伪元素的 content 改变文字内容。 -->
                <p class="des"></p>
            </div>
        </div>
    </div>
    <p class="info">盒子内部</p>
</div>
复制代码

这样的话限制很大,每个与鼠标交互的容器底下都得放置一个展示效果的盒子(每个 .box 类盒子下面都存放有一个单独的 .des 类盒子,.des 类盒子重复了不少次),虽然能实现精准度更高的鼠标进入方向判断,不过带来了不少重复代码,没什么用。

哦对了,本文中这两个例子的 CSS 代码性能都不好,当与鼠标交互的元素增长到 200 个左右开始,个人电脑就开始出现肉眼可见的卡顿了,这玩意儿生产环境是不可能用到的,也就只能应付应付魔王了。

嗯,其实仍是很好玩儿的对吧。(●ˇ∀ˇ●) 好玩儿就对了!

相关文章
相关标签/搜索