Vue.jsでCanvasを使ったローディングアニメーション作成
概要
Vue.js(Nuxt.js) + Vuetify
で開発している自社サイトに、オリジナルのローディング画面を作るためにCanvas
を利用してみました。
できあがったものはこちら。
ローディング画面の色をランダムにして全体的にパステル調?にしてみた pic.twitter.com/z0QeGf97vW
— 名人さん | ㈱NoSchool CTO (@Meijin_garden) September 17, 2019
Canvasの利用方法
要素のレンダリング
今回はVue.js
を使っているので、<template>
内に<canvas>
タグを置きます。ここではテンプレートエンジンにpug
を使っています。
<template lang="pug">
canvas(
width="200px"
height="30px"
ref="canvas"
)
</template>
ref
を貼ることでJavaScript側からアクセス可能にします。
コンテキストの取得
Canvas
を利用するには、<canvas>
要素からgetContext
メソッドでCanvas
を描画するためのコンテキストと呼ばれるオブジェクトを取得します。
this.ctx = this.$refs.canvas.getContext("2d");
よくインターネット上のコードでctx
という変数に格納されるため、便乗してctx
にしました。
最終的にローディング画面ごとVue
のコンポーネントにまとめるので、this.ctx
に保存しています。ローカル変数ではなくてメンバ変数に格納することでメソッドを切り分けても再度getContext
しなくていいようにしています。
ドットの描き方
今回僕が思いついたローディング画面は、ドットが足跡のように画面の左から右まで進んでいくというものです。したがって構成要素としてドットを描画できる必要があります。
ドットは実質「円」なので、下記の書き方で実現できます。
writeDot(step) {
this.ctx.fillStyle = '#239348';
this.ctx.beginPath();
this.ctx.arc(
200 - (170 / 6) * (6 - (step - 1)),
step % 2 === 1 ? 10 : 20,
5,
0,
Math.PI * 2
);
this.ctx.fill();
},
fillStyle
で色をまず指定して、beginPath
をおまじないで実行しておき、arc
で円を描きます。最後にfill
を実行することで描いた円を塗りつぶします。
arc
の引数はそれぞれX座標、Y座標、直径、描き始めの角度と書き終わりの角度です。角度の単位は高校数学あたりで履修するラジアン単位なので要注意ですが、円を描く場合はこの2つを渡すと覚えておけば差し支えないです。
座標の数式がちょっとややこしいですが、要は左から右に向かって一歩ずつドットを打ちながら、一歩ごとに少し上下にずれるのを表現しています。引数で現在の歩数であるstep
を受け取ってその歩数に従った箇所に描画します。
Canvasへの描画をアニメーションさせる
setInterval
を使って先程のwriteDot
メソッドを定期実行することで、一歩ずつ進んでいくようなローディングアニメーションを作成できます。
let step = 1;
this.canvasLoopId = setInterval(() => {
if (step > 6) {
this.ctx.clearRect(0, 0, 200, 30);
step = 1;
return;
}
this.writeDot(step++);
}, 200);
step
が現在の歩数です。だいたい200ミリ秒ごとに1歩にするといい感じだったのでそのように設定し、6歩進むごとに画面全体をclearRect
することでリセットしてやりなおし、というようにやっています。
アニメーションの開始と終了
ローディングを表示するコンポーネントなので、開始と終了をコンポーネントの外から指定できるといいのですが、コンポーネントの外から中のメソッドを直接実行するのは無理があるので、props
でstate
を渡してwatch
によってトリガを引くというようにしてみました。
props: {
state: {
type: Boolean,
required: true
}
},
watch: {
state(newState) {
if (newState) {
this.start();
} else {
this.stop();
}
}
},
コンポーネントの外から$refs.ref.hoge()
するようなやり方を提示するサイトもありますが、外のコンポーネントが内側のメソッド名に至るまで知っていないといけないのは運用上リスクが高いので、props
ベースでやってみようと思いました。
ダイアログ形式で表示
ローディング画面をダイアログで表示させるためにVuetify
のv-dialog
を利用しました。
<template>
タグの中身の全体はこのようになります。
<template lang="pug">
v-dialog(
persistent
width="200px"
height="60px"
:value="state"
)
.loading.pt-3.pb-1
p.mb-2.text-center 検索中...
canvas(
width="200px"
height="30px"
ref="canvas"
)
</template>
v-dialog
のvalue
にBooleanを渡すことで表示/非表示を切り替えることができるので、それに伴ってアニメーションを開始します。先程のwatch
との連携です。
※.loading
クラスには別途scoped scss
でスタイルを当てています。
余談ですが画面中央にモーダルを出すなどは地味に労力がかかるので、そういった基礎的かつ面倒な部分をUIフレームワークに投げられるのは便利だなと思いました。
ランダムにドットの色を変える
最後に、ドットの色をランダムにするために専用の関数を作りました。
getRandomColor() {
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const r = (getRandomInt(180) + 70).toString(16);
const g = (getRandomInt(70) + 180).toString(16);
const b = (getRandomInt(100) + 150).toString(16);
return `#${r}${g}${b}`;
}
こだわりとして、RGBのバランスを崩すことでグレーが強くならないようにするのと、全体的に明るめにすることでパステル調で可愛らしくなるように調整しています。
先程のwriteDot
メソッドを以下のように書き換えることで、ドットごとに色を変えることができます。
this.ctx.fillStyle = this.getRandomColor();
最終結果
全体のコードを貼るとこんな感じになっています。
最後の最後に非常に重要なことを言いますと、start
関数内でthis.$nextTick
を使っていることが欠かせないポイントです。
ちょうどwatch
をトリガーにstart
が実行されたタイミングかつ、v-dialog
でCanvasが表示されようとしているところなので、$nextTick
を噛まさずに進めると$refs
からcanvas
が取得できずエラーになります。ご注意を。
<template lang="pug">
v-dialog(
persistent
width="200px"
height="60px"
:value="state"
)
.loading.pt-3.pb-1
p.mb-2.text-center 検索中...
canvas(
width="200px"
height="30px"
ref="canvas"
)
</template>
<script>
export default {
props: {
state: {
type: Boolean,
required: true
}
},
watch: {
state(newState) {
if (newState) {
this.start();
} else {
this.stop();
}
}
},
methods: {
start() {
let step = 1;
this.$nextTick(() => {
this.ctx = this.$refs.canvas.getContext("2d");
this.clearCanvas();
this.canvasLoopId = setInterval(() => {
if (step > 6) {
this.clearCanvas();
step = 1;
return;
}
this.writeDot(step++);
}, 200);
});
},
clearCanvas() {
this.ctx.clearRect(0, 0, 200, 30);
},
writeDot(step) {
this.ctx.fillStyle = this.getRandomColor();
this.ctx.beginPath();
this.ctx.arc(
200 - (170 / 6) * (6 - (step - 1)),
step % 2 === 1 ? 10 : 20,
5,
0,
Math.PI * 2
);
this.ctx.fill();
},
stop() {
clearInterval(this.canvasLoopId);
},
getRandomColor() {
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const r = (getRandomInt(180) + 70).toString(16);
const g = (getRandomInt(70) + 180).toString(16);
const b = (getRandomInt(100) + 150).toString(16);
return `#${r}${g}${b}`;
}
},
data() {
return {
canvasLoopId: null
};
}
};
</script>
<style lang="scss" scoped>
@import "@/assets/css/main.scss";
.loading {
width: 200px;
background-color: $white;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
参考文献
Author And Source
この問題について(Vue.jsでCanvasを使ったローディングアニメーション作成), 我々は、より多くの情報をここで見つけました https://qiita.com/mejileben/items/d633ff64cd70c0d25ffe著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .