欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

實現(xiàn)抖音兩個旋轉(zhuǎn)小球的loading技巧實例

 更新時間:2023年05月11日 10:27:55   作者:夕水  
這篇文章主要為大家介紹了實現(xiàn)抖音兩個旋轉(zhuǎn)小球的loading技巧實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

探索小圓球加載效果實現(xiàn)原理

抖音的小圓球加載效果相信大家都見識過,也對其中的實現(xiàn)原理應(yīng)該有一定的好奇心吧,下面就讓我?guī)Т蠹襾硖剿饕幌滦A球加載效果的實現(xiàn)原理吧。

要實現(xiàn)兩個小圓球,我們可以思考兩種方案的實現(xiàn),第一種就是css方案,畫兩個小圓球,然后使用css動畫來實現(xiàn),而第二種則是使用canvas方案。我們首先來嘗試第一種方案,首先肯定是要畫出兩個小圓球,這不就是相當(dāng)于畫兩個圓嘛,所以使用寬高加圓角屬性即可實現(xiàn)。

html代碼如下

<div class="rotate-ball">
  <div class="small-ball small-left-ball"></div>
  <div class="small-ball small-right-ball"></div>
</div>

首先是一個旋轉(zhuǎn)的容器元素,接著就是左右兩個小圓球,我的思路也很簡單,既然兩個小球是互相旋轉(zhuǎn)的,那也就是說我給它們的父元素旋轉(zhuǎn)不就達(dá)到了兩個小球互相旋轉(zhuǎn)的效果嗎?

樣式代碼

.small-ball {
    width: 11px;
    height: 11px;
    border-radius: 50%;
}
.small-ball.small-left-ball {
    background-color: #e94359;
}
.small-ball.small-right-ball {
    background-color: #74f0ed;
}
.rotate-ball {
    width: 22px;
    animation: rotate 5s ease-in infinite forwards;
    transition: 2s;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
@keyframes rotate {
   0% {
      transform: rotateY(0deg);
   }
   100% {
      transform: rotateY(360deg);
   }
}

樣式代碼很簡單,就是設(shè)置兩個小圓球的寬高和圓角,然后分別設(shè)置不同的背景色,然后給父元素添加旋轉(zhuǎn)動畫,這看起來似乎很容易就實現(xiàn)了

嗯大功告成,等等,這個效果差的太遠(yuǎn)了吧,沒那么簡單,好吧很顯然這個方案不太合適,讓我們換一種方式來實現(xiàn),也就是第二種方案canvas方案。

使用canvas方案實現(xiàn)我們主要分為兩個步驟,第一步即使用canvas畫出兩個小圓球,第二步則是讓兩個小圓球進(jìn)行翻轉(zhuǎn),也就是添加翻轉(zhuǎn)動畫。

首先第一步當(dāng)然是畫小圓球,每個小圓球我們都可以看作是一個類,我們把它叫做ball,好的,接下來我們來看代碼如下:

class Ball { 
  // 這里寫核心代碼
}

既然小圓球是一個類,那么我們小圓球就會有屬性,思考一下,我們會有哪些屬性呢?總結(jié)如下:

  • 圓心X坐標(biāo)
  • 圓心Y坐標(biāo)
  • 半徑
  • 開始角度
  • 結(jié)束角度
  • 順時針,逆時針方向指定
  • 是否描邊
  • 是否填充
  • 縮放X比例
  • 縮放Y比例

首先小圓球有一個圓心坐標(biāo),既x和y坐標(biāo),其次還有半徑,然后旋轉(zhuǎn)的角度會有開始和結(jié)束,并且還會有旋轉(zhuǎn)的方向,然后就是畫小圓球是否有描邊,是否能夠填充,最后就是縮放比例(主要用于小球運(yùn)動時,我們根據(jù)實際效果可以看到小球旋轉(zhuǎn)的時候明顯有縮放效果,所以這里需要一個縮放比例的屬性)。

分析了屬性之后,很顯然我們第一步要做的就是初始化這些屬性,代碼如下:

class Ball {
  x: number;
  y: number;
  r: number;
  startAngle: number;
  endAngle: number;
  anticlockwise: boolean;
  stroke: boolean;
  fill: boolean;
  scaleX: number;
  scaleY: number;
  lineWidth: number;
  fillStyle: string | CanvasGradient | CanvasPattern;
  strokeStyle: string | CanvasGradient | CanvasPattern;
  constructor(o: AnyObj) {
    this.x = 0; // 圓心X坐標(biāo)
    this.y = 0; // 圓心Y坐標(biāo)
    this.r = 0; // 半徑
    this.startAngle = 0; // 開始角度
    this.endAngle = 0; // 結(jié)束角度
    this.anticlockwise = false; // 順時針,逆時針方向指定
    this.stroke = false; // 是否描邊
    this.fill = false; // 是否填充
    this.scaleX = 1; // 縮放X比例
    this.scaleY = 1; // 縮放Y比例
    this.init(o);
  }
  init(o: AnyObj): void {
    Object.keys(o).forEach(k => (this[k] = o[k]));
  }
}

初始化屬性之后我們要干什么?那當(dāng)然是渲染小圓球啦,寫一個render方法就可以了。

class Ball {
    // 以上代碼以省略
    render(){
      // 渲染小圓球代碼
    }
}

canvas畫圓的步驟

如何畫小圓球?也就是canvas畫圓的步驟,最核心的就是canvas的arc方法,總的說來,我們主要分為設(shè)置原點(diǎn)坐標(biāo),設(shè)置縮放,調(diào)用arc方法畫圓,設(shè)置線寬,填充顏色,以及描邊這幾步,然后我們返回小圓球?qū)嵗蚨a如下:

class Ball {
    // 以上代碼已省略
    render(ctx: CanvasRenderingContext2D | null): Ball | void {
    if (!ctx) {
      return;
    }
    ctx.save();
    ctx.beginPath();
    ctx.translate(this.x, this.y); // 設(shè)置原點(diǎn)的位置
    ctx.scale(this.scaleX, this.scaleY); // 設(shè)定縮放
    ctx.arc(0, 0, this.r, this.startAngle, this.endAngle); // 畫圓
    if (this.lineWidth) {
      // 線寬
      ctx.lineWidth = this.lineWidth;
    }
    if (this.fill) {
      // 是否填充
      this.fillStyle ? (ctx.fillStyle = this.fillStyle) : null;
      ctx.fill();
    }
    if (this.stroke) {
      // 是否描邊
      this.strokeStyle ? (ctx.strokeStyle = this.strokeStyle) : null;
      ctx.stroke();
    }
    ctx.restore();
    return this;
  }
}

如此一來,我們畫小圓球這一步就算是完成了,接下來我們要做的就是讓小圓球動起來。要讓小圓球動起來,那么就需要用到定時器,然而我這里并沒有使用setInterval函數(shù),這是為什么呢?

如果我們把定時器每次執(zhí)行一次看作是一個任務(wù),那么setInterval就相當(dāng)于是按照一定的時間間隔來執(zhí)行任務(wù),而一個任務(wù)的開始時間和結(jié)束時間我們是無法保證它們之間的時間間隔的,也就是說有時候我們的循環(huán)定時任務(wù)會被跳過,而setTimeout因為是在條件滿足的時候會自動停止,所以我們可以使用setTimeout來避免這個問題,因此,接下來我要說的就是我們會使用setTimeout來模擬實現(xiàn)setInterval函數(shù)。

那么如何實現(xiàn)呢?

我們把每次setTimeout執(zhí)行也看作是一個任務(wù),然后我們通過一個對象來存儲每一次執(zhí)行的任務(wù),這樣我們每次執(zhí)行的任務(wù)都可以通過在對象當(dāng)中找到,因此,我們要清除任務(wù)同樣也可以從對象當(dāng)中取出任務(wù)來清除。

也就是說,我們存儲每一個setTimeout任務(wù)的延遲id,這個函數(shù)返回一個數(shù)值型的延遲id,我們把這個值記錄到對象當(dāng)中,方便后面從對象當(dāng)中取出然后清除任務(wù)。

實現(xiàn)這個函數(shù)主要分成兩部分,第一部分當(dāng)然還是模擬實現(xiàn)執(zhí)行定時任務(wù),第二部分就是模擬實現(xiàn)一個清除定時任務(wù)的函數(shù),即clearInterval函數(shù)的模擬實現(xiàn)。

模擬實現(xiàn)定時任務(wù)我們可以使用遞歸來實現(xiàn),這個應(yīng)該還是比較好理解,這里我們還有一點(diǎn),那就是存儲在對象當(dāng)中的延遲id,我們需要一個屬性名,對象不就是一種含有屬性名屬性值的鍵值對數(shù)據(jù)類型嗎?在這里屬姓名我們可以使用Symbol類型,為什么使用這個數(shù)據(jù)類型?因為這個數(shù)據(jù)類型確保了唯一性。

最后還有一點(diǎn),那就是如果要寫ts類型,那么定時器任務(wù)的回調(diào)函數(shù)應(yīng)該是任意類型的函數(shù),因此這里需要編寫類型代碼。如下:

type AnyFunction = (...args: any) => any;

模擬函數(shù)代碼

通過以上分析,我們的模擬函數(shù)代碼就很好理解了,代碼如下:

export const defineSetInterval = (): {
  setInterval: (fn: AnyFunction, time: number) => symbol;
  clearInterval: (k: symbol) => void;
} => {
  const timeWorker = {};
  const key = Symbol();
  const defineInterval = (handler: AnyFunction, interval: number) => {
    const executor = (fn: AnyFunction, time: number) => {
      timeWorker[key] = setTimeout(() => {
        fn();
        executor(fn, time);
      }, time);
    };
    executor(handler, interval);
    return key;
  };
  const defineClearInterval = (k: symbol):void => {
    if (k in timeWorker) {
      clearTimeout(timeWorker[k] as number);
      delete timeWorker[k];
    }
  };
  return {
    setInterval: defineInterval,
    clearInterval: defineClearInterval
  };
};

以下是該函數(shù)的使用示例代碼:

const { setInterval, clearInterval } = defineSetInterval();
const timeId = setInterval(() => alert('hello,world!'), 1000);
// 取消定時器// clearInterval(timeId);

讓我們繼續(xù)下一步,下一步我們當(dāng)然是創(chuàng)建這兩個小圓球,然后暴露出一個start方法和一個clear方法,顧名思義,就是在這個函數(shù)當(dāng)中我們創(chuàng)建小圓球,然后默認(rèn)不執(zhí)行動畫,將執(zhí)行動畫的邏輯包裝在start方法中,而之所以留下一個clear方法,那就是如果需要實現(xiàn)暫停效果,也就是清除定時器了,那么我們就需要調(diào)用clear方法清除定時器,暫停執(zhí)行動畫,如果需要重新執(zhí)行動畫,那么我們也就重新調(diào)用start方法即可。因此這個函數(shù)的結(jié)構(gòu)我們可以定義如下:

export interface CreateBallReturnType {
  clear: () => void;
  start: (time?: number) => void;
}
export interface AnyObj {
    [prop: string]: unknown
}
export const createBall = (
  el: HTMLElement | string,
  leftBallConfig?: AnyObj,
  rightBallConfig?: AnyObj
): CreateBallReturnType => {
  // 這里寫核心代碼
}

這個函數(shù)有3個參數(shù),第一個參數(shù)是一個dom元素,也就是說,我們需要將兩個小圓球渲染到canvas元素上,再將這個canvas元素添加到一個容器元素當(dāng)中,這個el參數(shù)就是代表傳入一個容器元素中,如果不傳,那么我們默認(rèn)就添加到body元素中,第二個和第三個參數(shù)分別是兩個小圓球的配置屬性對象,其實這里我們直接采用默認(rèn)的就好,不需要傳入這兩個參數(shù),因此這兩個參數(shù)是可選的,雖然這里定義的是任意對象,但實際上根據(jù)前面小圓球類含有哪些屬性的分析結(jié)果來看,這兩個參數(shù)很明顯傳入的就是初始化的那些屬性,如果有特別需求,可以傳入這些屬性進(jìn)行更改。

計算縮放比例的公式

在實現(xiàn)該函數(shù)的核心之前,我們這里會涉及到一個計算縮放比例的公式,代碼如下:

export const computedScale = (val: number, dir: number, dis: number): number =>
  (val * 1000 + dir * (dis * 1000)) / 1000;

這里就不多分析這個公式的原理了,只要記住它是一個公式就可以了。

接下來我們看該函數(shù)的核心實現(xiàn),我們主要也還是分成2個部分,第一個部分渲染兩個小圓球并添加到容器元素中,定義動畫函數(shù),并封裝到start函數(shù)當(dāng)中,然后暴露出start和clear函數(shù)。這里需要注意的一點(diǎn),那就是小圓球的寬高以及canvas元素的寬高不會太大,然后小圓球移動有個邊界,因此x坐標(biāo)和y坐標(biāo)有個最小值和最大值,我們定義成一個一維數(shù)組即可。

接下來,我們按照相應(yīng)的分析步驟去實現(xiàn)每一步驟的代碼就可以了,每一步在代碼當(dāng)中也有所注明,所以我們只需要看完整代碼即可。

export const createBall = (
  el: HTMLElement | string,
  leftBallConfig?: AnyObj,
  rightBallConfig?: AnyObj
): CreateBallReturnType => {
  const container = (typeof el === 'string' ? document.querySelector(el) : el) || document.body;
  const canvas = document.createElement('canvas');
  canvas.width = 34;
  canvas.height = 20;
  container.appendChild(canvas);
  const w = canvas.width;
  const h = canvas.height;
  const ctx = canvas.getContext('2d');
  const xArr = [10, 22];
  const yArr = [10, 10];
  const leftBall = new Ball({
    x: xArr[0],
    y: yArr[0],
    r: 6,
    startAngle: 0,
    endAngle: 2 * Math.PI,
    fill: true,
    fillStyle: '#E94359',
    lineWidth: 1.2,
    ...leftBallConfig
  }).render(ctx);
  const rightBall = new Ball({
    x: xArr[1],
    y: yArr[1],
    r: 6,
    startAngle: 0,
    endAngle: 2 * Math.PI,
    fill: true,
    fillStyle: '#74F0ED',
    lineWidth: 1.2,
    ...rightBallConfig
  }).render(ctx);
  const a = 1.04; // 加速度
  let dir = 1; // 方向
  let dis = 1; // X軸移動初始值
  const move = (): void => {
    if (!ctx || !leftBall || !rightBall) {
      return;
    }
    // 清理畫布
    ctx.clearRect(0, 0, w, h);
    // 通過加速度計算移動值
    dis *= a;
    // 更改x軸位置
    leftBall.x += dir * dis;
    rightBall.x -= dir * dis;
    // 計算縮放比例
    leftBall.scaleX = computedScale(-dir, 0.005, leftBall.scaleX);
    leftBall.scaleY = computedScale(-dir, 0.005, leftBall.scaleY);
    rightBall.scaleX = computedScale(dir, 0.005, rightBall.scaleX);
    rightBall.scaleY = computedScale(dir, 0.005, rightBall.scaleY);
    // 到達(dá)指定位置后
    if (leftBall.x >= 22 || rightBall.x >= 22 || leftBall.x <= 10 || rightBall.x <= 10) {
      // 設(shè)定縮放比例
      leftBall.scaleX = rightBall.scaleX;
      leftBall.scaleY = rightBall.scaleY;
      rightBall.scaleX = leftBall.scaleX;
      rightBall.scaleY = leftBall.scaleY;
      // 還原X軸移動初始值
      dis = 1;
      // 變更移動方向
      dir = -dir;
    }
    // 繪制
    if (dir > 0) {
      // 方向不一樣時,小球的繪制順序要交換,移模擬旋轉(zhuǎn)
      rightBall.render(ctx);
      leftBall.render(ctx);
    } else {
      leftBall.render(ctx);
      rightBall.render(ctx);
    }
  };
  let timer: symbol;
  const { setInterval: setHandler, clearInterval: clearHandler } = defineSetInterval();
  const start = (time = 50): void => {
    timer = setHandler(move, time);
  };
  return {
    start,
    clear: (): void => {
      if (timer) {
        clearHandler(timer);
      }
    }
  };
};

可以看到我們先是創(chuàng)建canvas元素,設(shè)置寬高,然后創(chuàng)建兩個小圓球添加到canvas元素當(dāng)中,再然后我們定義一個move方法,也就是小圓球的翻轉(zhuǎn)動畫的實現(xiàn),難點(diǎn)可能就主要是翻轉(zhuǎn)動畫的實現(xiàn)原理。

翻轉(zhuǎn)動畫的實現(xiàn)原理

如此一來,我們?nèi)绻菍慾s/ts代碼,使用起來就很簡單,直接調(diào)用方法即可,如:

createBall();
// 如果需要指定特定的容器元素,那么傳入一個dom元素,例如 document.querySelector('#app')
// 又或者傳入一個字符串也可以,既'#app'
// 也就是createBall('#app');

接下來我們再封裝一下,將這個函數(shù)用到react框架中,做成一個組件,很簡單,我們利用ref對象來存儲dom元素,然后使用useEffect函數(shù)監(jiān)聽這個dom元素是否存在,然后存在就調(diào)用該方法。代碼如下:

import React, { createRef, CSSProperties, ReactElement, useEffect } from 'react';
import { createBall } from './ball';
import '../style/loading.scss';
export interface LoadingProps extends AnyObj {
  style?: CSSProperties;
}
const Loading = (props: LoadingProps = {}): ReactElement | null => {
  const loadingRef = createRef<HTMLDivElement>();
  useEffect(() => {
    // 這里多一個children判斷是因為如果該元素已經(jīng)被渲染過,我們就不需要添加到容器元素中了
    if (loadingRef.current && !loadingRef.current.children.length) {
      const ball = createBall(loadingRef.current);
      ball.start();
    }
  }, [loadingRef]);
  return <div ref={loadingRef} className="loading" {...props} />;
};
export default Loading;

這里涉及到了一點(diǎn)樣式,樣式隨便自己寫:

@import './extend.scss';
.loading {
  position: absolute;
  left: 0;
  top: 0;
  @extend .perfect, .flex-center;
}

extend.scss代碼如下:

.flex-content-center {
  display: flex;
  justify-content: center;
}
.flex-align-center {
  display: flex;
  align-items: center;
}
.flex-center {
  @extend .flex-content-center, .flex-align-center;
}
.perfect {
  width: percentage(1);
  height: percentage(1);
}

如此一來,我們的抖音旋轉(zhuǎn)小圓球效果就實現(xiàn)了,更多關(guān)于抖音兩個旋轉(zhuǎn)小球loading的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論