【地獄難度】面試官:你能夠用純 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 個左右開始,個人電腦就開始出現肉眼可見的卡頓了,這玩意兒生產環境是不可能用到的,也就只能應付應付魔王了。

嗯,其實仍是很好玩兒的對吧。(●ˇ∀ˇ●) 好玩兒就對了!

相關文章
相關標籤/搜索