学習の備忘録です。タスク管理アプリを作っています。
今回は、スタンプカードのアニメーション演出を実装していきます。
スタンプ獲得のバッチ処理については以下、
スタンプカードのモーダル表示ついては以下の記事にまとめていますので、そちらも参考にしていただければ幸いです。
本記事は個人の学習記録です。内容の正確性を保証するものではありませんので、あらかじめご了承ください。
はじめに:バッチ処理の記述を削除
public function handle()
{
$targetDate = today()->subDay(); // 昨日の日付
$users = User::with(['tasks' => function($query) use ($targetDate) {
$query->whereDate('task_date', $targetDate);
}, 'userStat'])->get();
foreach ($users as $user) {
$tasks = $user->tasks;
if ($tasks->isEmpty()) continue;
//達成率取得
$rate = Task::calAchievementRate($tasks);
//達成率が100%の時の処理
if ($rate === 100) {
$user->userStat->increment('perfect_stamp_count'); //←ここを削除
StampHistory::firstOrCreate([
'user_id' => $user->id,
'stamp_date' => $targetDate,
], [
'is_read' => false,
]);
}
}
$this->info('スタンプ判定完了: ' . $targetDate);
}
バッチ処理のコマンドクラスのうち、「$user->userStat->increment(‘perfect_stamp_count’);」を削除します。
理由としては、スタンプカードでスタンプを表示させる際、このバッチ処理でスタンプ数を増やしてしまうと、「今からスタンプ獲得のアニメーション演出をしたいマスに、既にスタンプ獲得済になっている」という事態になってしまうからです。
よって、バッチ処理の段階ではスタンプ数は増やさず、「アニメーション演出が済んだらスタンプ数を増やす」という処理に変更します。
トップ画面表示のコントローラー
トップ画面を表示させるshowIndex()メソッドに、以下の記述を追加します。
//未読スタンプを取得
$unreadStamps = StampHistory::where('user_id',$user->id)
->where('is_read', false)
->count();
return view('tasks.index', compact('tasks', 'displayDate', 'prevDate', 'nextDate', 'rate', 'editTickets', 'stampCount', 'unreadStamps'));
HTML:アニメーション用にクラス追加&js追加
元々のコードはこんな感じです。
<div class="stamp-modal-content">
<div class="stamp-text-area">
<p class="stamp-text">STAMP</p>
</div>
<div class="stamp-grid">
@for ($i=0; $i < 10; $i++)
<div class="stamp-item">
@if ($i < $stampCount)
<p class="stamp-mark">●</p>
@endif
</div>
@endfor
</div>
</div>
これについて、以下のように改修します。
<div class="stamp-modal-content">
<div class="stamp-text-area">
<p class="stamp-text">STAMP</p>
</div>
<div class="stamp-grid">
@for ($i=0; $i < 10; $i++)
<div class="stamp-item">
@if ($i < $stampCount + $unreadStamps)
<p class="stamp-mark {{ $i >= $stampCount ? 'stamp-animate' : '' }}">●</p>
@endif
</div>
@endfor
</div>
</div>
@if($unreadStamps > 0)
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#stamp-card-modal').addClass('is-open');
$('.stamp-animate').addClass('pop');
setTimeout(function() {
$.ajax({
url: '{{ route("stamp.confirm") }}',
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
success: function(data) {
console.log('Ajax成功:', data);
console.log('newCount:', {{ $stampCount + $unreadStamps }});
console.log('stamp-icon要素:', $('.stamp-icon').length); // 0なら要素が取れていない
if (data.success) {
const newCount = {{ $stampCount + $unreadStamps }};
$('.stamp-icon').attr('src', '{{ asset('images/') }}' + 'stamp_' + newCount + '.png');
setTimeout(function() {
$('#stamp-card-modal').removeClass('is-open');
}, 2300);
}
}
});
}, 800);
});
</script>
@endif
console.log('Ajax成功:', data);
console.log('newCount:', {{ $stampCount + $unreadStamps }});
console.log('stamp-icon要素:', $('.stamp-icon').length); // 0なら要素が取れていない
これはちゃんとajaxが動いてるかの確認用なので無視でも大丈夫です。
獲得済スタンプ+未獲得スタンプでスタンプ表示マス判定
@if ($i < $stampCount + $unreadStamps)
ここについて、「もしもそのマス($i)が獲得済スタンプ+未獲得スタンプの合計より値が小さければ、スタンプを表示させる」という分岐です。
つまり、獲得済スタンプが3、未獲得スタンプが2の場合、5マスまでスタンプを表示させます。
そして、
<p class="stamp-mark {{ $i >= $stampCount ? 'stamp-animate' : '' }}">●</p>
ここで「表示させるスタンプがアニメーション演出済なのか、まだなのか」を判定します。
獲得済スタンプが3、未獲得スタンプが2の場合、そのマス($i)が獲得済スタンプ数以上なら、まだアニメーション演出をしていないスタンプだと判定できます。
マス1($i=0)→ 過去に獲得済み → 既にスタンプが入っている → アニメーション不要
マス2($i=1)→ 過去に獲得済み → 既にスタンプが入っている → アニメーション不要
マス3($i=2)→ 過去に獲得済み → 既にスタンプが入っている → アニメーション不要
マス4($i=3)→ 今回新たに獲得 → 今ポンと押す演出をする → アニメーション必要
マス5($i=4)→ 今回新たに獲得 → 今ポンと押す演出をする → アニメーション必要
マス6〜10 → まだ獲得していない → 空マス
jQueryを追加
jsファイルに分けたらbladeで使っていた変数そのまま参照できなくなり、わざわざデータ通信用に色々記述しないといけないので、それがめんどくさくてHTMLの中に直接書きました。
処理の流れとしては以下です。
- スタンプカードのモーダルを開く
- 「.stamp-animate」が追加されたマスにさらに「pop」クラスを追加
- 非同期処理で800ミリ秒後にコントローラーと通信(データベース更新)
- コントローラーの処理が成功したらトップ画面の画像を切り替え
- 画像切り替えまで終わったら2300ミリ秒後にモーダルを閉じる
CSSを追加
該当箇所のCSSは以下です。
.stamp-mark.pop {
animation: stampPop 0.4s ease-out forwards;
}
@keyframes stampPop {
0% { transform: scale(2.5); opacity: 0; }
55% { transform: scale(0.85); opacity: 1; }
75% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@keyframesの流れを説明すると、
- 大きくて透明なスタンプを作る
- 標準のサイズよりやや縮小したスタンプを表示させる
- 標準のサイズよりやや膨張したスタンプを表示させる
- 標準のサイズに落ち着く
跳ねるような、ブレるようなアニメーションになります。
animation: stampPop 0.4s ease-out forwards;
↑名前 ↑時間 ↑イージング ↑終了後も最終状態を維持
イージングというのは動きの緩急を表します。
イージングの概念については、私は以下のサイトを参考にしました。
forwardsは100%の状態のまま止まる指定で、これがないとアニメーション終了後に元の状態に戻ってしまいます。
ルーティング
//スタンプ獲得
Route::post('/stamp/confirm', [StampController::class, 'confirmStamp'])->name('stamp.confirm');
ルーティングを追加します。
コントローラーを新規に作ってルート設定した場合は、useのところにコントローラーを追加するのを忘れないようにしましょう。
use App\Http\Controllers\StampController;
スタンプ獲得処理のコントローラー
TaskControllerに処理書くとごちゃつくなと思ったのでStampControllerを新たに作りました。
処理の内容はこんな感じ。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\StampHistory;
use Illuminate\Support\Facades\DB;
class StampController extends Controller
{
//スタンプ獲得
public function confirmStamp(Request $request) {
//ログインユーザー取得
$user = User::findByToken($request);
//未読スタンプを取得
$unreadStamps = StampHistory::where('user_id',$user->id)
->where('is_read', false)
->count();
if ($unreadStamps > 0) {
$user->userStat->increment('perfect_stamp_count', $unreadStamps);
StampHistory::where('user_id', $user->id)
->where('is_read', false)
->update(['is_read' => true]);
}
return response()->json(['success' => true]);
}
}
流れとしては以下です。
- ログインユーザーを取得
- 未読スタンプ数を取得
- 未読スタンプがあるなら、user_statsテーブルのperfect_stamp_countに未読分を全て追加
- stamp_historiesテーブルの該当ユーザーのデータの中で、is_readがfalseのものを全てtrueにアップデートする
- 処理が終わったらjs側にデータを渡す
increment(‘perfect_stamp_count’, $unreadStamps)について、
increment(‘perfect_stamp_count’)だけだと自動的に+1されますが、第二引数に値を入れるとその値の分だけ加算してくれます。
まとめ
以上で、未読スタンプのアニメーション演出をして、スタンプを獲得する処理が完了しました。
バッチ処理は毎日する想定なので、一人のユーザーが未読スタンプをいくつも抱えることはないとは思いますが、万が一未読スタンプが複数個になってしまった時に備えた作りにしています。





コメント