
ホームページのトップに、ちょっとした動きがあると、会社の雰囲気が伝わりやすくなります。とはいえ、CSSだけでは表現できる動きに限りがあり、マウスに反応させるといった「さわれる動き」は、動画では作れません。
Cruwでは、2026年9月のサイトリニューアルで、トップページのメインビジュアルに JavaScriptのCanvas(キャンバス) で描いた風景を加えました。以前からある背景の動画はそのまま残し、その上にCanvasを重ねています。長野らしく山に囲まれた湖を、帆にロゴを掲げたヨットが進み、季節によって山の色と舞うものが変わります。マウスを動かすと船が舵を切り、跳ねた魚をクリックすると釣れる「こっそりゲーム」も入っています。
この記事では、その作り方を、実際のコードから抜き出して紹介します。
- 波を、sin(サイン)関数を重ねて自然に動かす方法
- 月によって、色や舞うもの(花びら・落ち葉・雪)を切り替える仕組み
- マウスとの連動、隠しミニゲーム、重くしないための工夫
結論:描画ループの中で「更新」と「描画」をくり返す
Canvasのアニメーションは、パラパラ漫画と同じです。requestAnimationFrame で1秒間に何十回も、「位置を少し動かす(更新)」→「画面を消して描き直す(描画)」をくり返します。
Cruwのメインビジュアルでは、次の順番で、奥のものから手前のものへと重ねて描いています。
| 描く順番 | ポイント |
|---|---|
| 1. 山と、湖面への映り込み | マウスに合わせて少しずらし、奥行きを出す |
| 2. 奥の波(2層) | 半透明の水色 |
| 3. 航跡 | ロゴと同じ「く」の字 |
| 4. ヨットと、跳ねる魚 | 波の高さに合わせて上下・傾く |
| 5. 手前の波 | 船の下の部分を隠して、水に浮かんで見せる |
| 6. 舞うもの・鳥・泡 | 季節で変わる |
手前の波を船より後に描くのがコツです。船の底が波に隠れて、水に浮かんでいるように見えます。
波は、sin関数を2つ重ねて作る
波の高さは、横の位置 x と時間 t から計算します。sin関数を1つだけ使うと、きれいすぎて機械的に見えるため、長さと速さの違うsinをもう1つ足して います。
// 奥から手前へ3層の波(湖なので穏やかに)
var LAYERS = [
{ amp: 5, len: 0.0065, speed: 0.45, offset: -26, color: 'rgba(0,178,255,0.18)' },
{ amp: 7, len: 0.0048, speed: 0.65, offset: -8, color: 'rgba(0,151,219,0.24)' },
{ amp: 6, len: 0.0082, speed: 0.9, offset: 22, color: null } // 手前は白のグラデーション
];
function waveY(layer, x, t) {
return seaTop + layer.offset +
Math.sin(x * layer.len - t * layer.speed) * layer.amp +
Math.sin(x * layer.len * 2.3 - t * layer.speed * 1.7 + 1.3) * layer.amp * 0.35 +
rippleOffset(x); // マウスで立てた波紋の分
}| 値 | 意味 |
|---|---|
amp | 波の高さ(振幅)。湖なので5〜7pxと控えめに |
len | 波の細かさ。大きいほど波が短くなる |
speed | 波の進む速さ。奥ほど遅くすると、奥行きが出る |
offset | 層ごとの上下の位置 |
描くときは、画面の左から右へ8pxずつ waveY で高さを求めて線を引き、下の端まで囲んで塗りつぶします。1pxずつ計算するより軽く、見た目はほとんど変わりません。
function drawLayer(layer, t) {
ctx.beginPath();
ctx.moveTo(0, H);
for (var x = 0; x <= W + 8; x += 8) {
ctx.lineTo(x, waveY(layer, x, t));
}
ctx.lineTo(W, H);
ctx.closePath();
if (layer.color) {
ctx.fillStyle = layer.color;
} else {
// 手前の層は、上が半透明・下が白のグラデーション
var g = ctx.createLinearGradient(0, seaTop, 0, H);
g.addColorStop(0, 'rgba(255,255,255,0.55)');
g.addColorStop(0.6, 'rgba(255,255,255,1)');
ctx.fillStyle = g;
}
ctx.fill();
}ヨットや跳ねる魚も、この waveY で求めた湖面の高さに合わせて置いています。船の前後2点の高さの差から傾きも計算しているので、波に合わせてゆらゆらと揺れます。
季節で色と舞うものを切り替える
季節ごとの違いは、設定の表(オブジェクト)を1つ用意して、月で選ぶだけ にしています。描く処理そのものは、季節に関係なく同じです。
var SEASONS = {
spring: { mountains: ['#d4e4ee', '#b5d6c1', '#e3efe6', '#c8e1cf'], snow: 0.84, particle: 'petal' },
summer: { mountains: ['#bcd5e8', '#8ec2a2', '#d0e7d9', '#a8d3b7'], snow: 0, particle: 'sparkle' },
autumn: { mountains: ['#d3dbe6', '#e0a06a', '#f1dcc2', '#e9bd90'], snow: 0.9, particle: 'leaf' },
winter: { mountains: ['#b3c9de', '#d3e2ef', '#e9f1f8', '#d6e4f0'], snow: 0.42, particle: 'snow' }
};
function currentSeason() {
// ?season=winter などで確認用に切り替えられる
var q = /[?&]season=(spring|summer|autumn|winter)/.exec(location.search);
if (q) return q[1];
var m = new Date().getMonth() + 1;
if (m >= 3 && m <= 5) return 'spring';
if (m >= 6 && m <= 8) return 'summer';
if (m >= 9 && m <= 11) return 'autumn';
return 'winter';
}
| 季節 | 山 | 舞うもの |
|---|---|---|
| 春(3〜5月) | 若葉の緑、高い山頂に残雪 | 桜の花びら |
| 夏(6〜8月) | 濃い緑、雪なし | 湖面のきらめき |
| 秋(9〜11月) | 紅葉のオレンジ、いちばん高い山頂にうっすら雪 | 落ち葉 |
| 冬(12〜2月) | 白っぽい青、雪が多い | 雪 |
snow は、右奥の連なった山(アルプス)のうち「どの高さより上の山頂に雪をかぶせるか」の値で、冬は低い峰まで雪をかぶります。背景の光の色も、CSS変数(--orb1 など)に季節の色を入れて切り替えています。
URLの最後に ?season=winter などを付けると、季節を待たずに確認できるようにしています。制作中の確認や、お客様に見ていただくときに便利です。
ロゴの「く」の字を、航跡として広げる
Cruwのロゴの模様は、「船の舵を取る」「船が進んだあとに広がる波」を表しています。メインビジュアルでは、その「く」の字を、ヨットの後ろに広がる 航跡(船の通ったあとの波) として描いています。

「く」の字は、上の線をロゴと同じ青のグラデーション、下の線をグレーにした、2本の曲線です。航跡は船尾から生まれ(船が速いほど短い間隔で生まれます)、時間とともに大きく、薄くなって消えていきます。帆に描いたロゴも、同じ関数を4つ並べて描いています。
// ロゴの「く」の字(上が青、下がグレー)
function chevron(x, y, size, alpha, width, tilt, spread, dir) {
var ux = Math.cos(spread) * size * -dir;
var uy = Math.sin(spread) * size;
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.globalAlpha = alpha;
var g = ctx.createLinearGradient(x, y, x + ux, y - uy);
g.addColorStop(0, '#0097db');
g.addColorStop(1, '#0b318f');
ctx.strokeStyle = g;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + ux * 0.5, y - uy * 0.5 + tilt, x + ux, y - uy);
ctx.stroke();
ctx.strokeStyle = '#9fa0a0';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + ux * 0.5, y + uy * 0.5 + tilt, x + ux, y + uy);
ctx.stroke();
ctx.globalAlpha = 1;
}会社のロゴや色を、そのまま動きのモチーフにすると、ただの飾りではなく「その会社らしさ」を伝える動きになります。
マウスとの連動と、隠しミニゲーム
マウスに合わせて、船が舵を切る
マウスを動かすと、ヨットがその方向へ進みます(スマートフォンはタップした方向)。目標の位置へ一気に移動させるのではなく、速度を少しずつ目標に近づける ことで、船らしいゆったりした動きにしています。
function updateBoat(dt) {
var target = boat.target;
// 5秒以上操作がなければ、ゆったり行ったり来たり
if (target === null || clock - lastInput > 5) {
target = boat.home + Math.sin(clock * 0.18) * W * 0.12;
}
var diff = target - boat.x;
var maxSpeed = isSP ? 90 : 170;
var desired = Math.max(-maxSpeed, Math.min(maxSpeed, diff * 1.4));
boat.v += (desired - boat.v) * Math.min(1, dt * 2.2); // 速度を少しずつ近づける
boat.x += boat.v * dt;
// 進む向きに船首を向ける(小刻みに反転しないよう余裕を持たせる)
if (boat.v < -12) boat.dir = -1;
else if (boat.v > 12) boat.dir = 1;
}ほかにも、マウスの位置に合わせて、奥の山ほど小さく、手前の山ほど大きく横にずらしています(パララックス)。湖面の近くでマウスを動かすと、波紋と泡が立ちます。
跳ねた魚をクリックすると釣れる
湖では、ときどき魚が跳ね、鳥の群れが飛んでいきます。実は、跳ねている魚をクリック(タップ)すると釣れる ようになっています。説明はどこにも書いていない、こっそりゲームです。釣った数が表示されたり、珍しい魚が出たりする仕掛けもありますが、それはぜひトップページで探してみてください。
当たり判定は、魚の今の位置と、クリックした位置の距離を比べるだけです。スマートフォンは指で押すので、判定の範囲を少し広げています。
function hitFish(x, y) {
var r = isSP ? 40 : 32; // スマホは指なので広めに
for (var i = fishes.length - 1; i >= 0; i--) {
var p = fishPos(fishes[i]);
if (Math.abs(p.x - x) < r && Math.abs(p.y - y) < r) return i;
}
return -1;
}釣った数は、ブラウザの localStorage に保存しているので、次にサイトを開いたときも続きから数えられます。プライベートブラウズなどで保存できない環境もあるため、読み書きは try で囲み、保存できなくてもゲーム自体は動く ようにしています。
var catchData = { count: 0, gold: 0 };
try {
var saved = JSON.parse(localStorage.getItem('cruw_mv_fish') || 'null');
if (saved && typeof saved.count === 'number') catchData = saved;
} catch (e) {}会社のサイトにゲームを入れるのは遊びすぎに見えるかもしれません。ただ、気づいた人が「おっ」と思ってくれることで、会社の名前を覚えてもらうきっかけになります。採用サイトや、技術力を伝えたいサイトとも相性のよい演出です。
重くしないための工夫
トップページのメインビジュアルは、すべての人が最初に見る場所です。動きがあっても、ページの表示を遅くしたり、スマートフォンの電池を減らしたりしない ことを大切にしました。
| 工夫 | やっていること |
|---|---|
| 見えないときは止める | IntersectionObserver で画面の外にスクロールしたとき、visibilitychange で別のタブを見ているときは、描画ループを止める |
| 高解像度は2倍まで | devicePixelRatio に合わせてくっきり描くが、3倍の画面でも2倍で止めて、描く量を抑える |
| スマホは数を減らす | 舞うものはPC 26個・スマホ12個など、画面の幅で数を変える |
| 動きを減らす設定を尊重する | OSで「動きを減らす」などの設定をしている人には、prefers-reduced-motion で判定して、動かない1枚の絵だけを描く |
| 時間の飛びを抑える | 処理が一瞬重くなって描画が遅れたときも、1回の更新で進む時間を0.05秒までに制限し、船や魚が瞬間移動しないようにする。描画を止めたあとに再開するときは、時間をリセットしてから始める |
| 外部ライブラリを使わない | 素のJavaScriptだけで書き、読み込むファイルを増やさない |
特に「見えないときは止める」は、効果が大きい工夫です。メインビジュアルを過ぎて下の内容を読んでいる間も描き続けると、見えないもののために電池を使い続けることになります。
function sync() {
if (visible && !document.hidden) start();
else stop();
}
document.addEventListener('visibilitychange', sync);
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
sync();
}).observe(root);よくある質問
CSSアニメーションとCanvas、どちらを使えばいいですか?
ボタンや文字がふわっと出てくるなど、HTMLの要素を動かすだけならCSSアニメーションで十分で、手軽です。波の形が毎回変わる、たくさんのものが同時に動く、マウスの位置で動きが変わる、といった表現は、Canvasのほうが向いています。Cruwのメインビジュアルでも、キャッチコピーはHTMLの文字、湖の風景はCanvasと、使い分けています。
Canvasで描いたものは、SEOに影響しますか?
Canvasの中に描いた絵や文字は、検索エンジンには読まれません。画面の読み上げソフトにも伝わらないため、Cruwでは <canvas> に aria-hidden="true" を付けて、読み上げの対象から外しています。会社名やキャッチコピーなど、伝えたい文字は、Canvasの上に重ねたHTMLのテキストで表示しています。Canvasは「雰囲気を伝える背景」と割り切るのがおすすめです。
スマートフォンでも動きますか?
動きます。マウスの代わりにタップで船が動き、魚も釣れます。画面が小さいので、船や魚を小さめにし、舞うものの数を減らしています。
まとめ
- Canvasのアニメーションは、
requestAnimationFrameで「更新」と「描画」をくり返して作ります。波は、sin関数を2つ重ねると自然に見えます - 季節ごとの違いは、色や舞うものを設定の表にまとめ、月で選ぶだけにすると、描く処理を変えずに切り替えられます
- 見えないときは止める、高解像度は2倍まで、動きを減らす設定を尊重する、などの工夫で、動きがあっても軽いページにできます
Canvasを使ったアニメーションは、Canvasを使ったカッコいいドットアニメーションでも紹介しています。また、「ぬりえ図鑑」のトップページに、さわって遊べるアニメーションを入れた事例は、こちらの記事で紹介しています。
Cruwでは、会社のロゴや地域の風景など、「その会社らしさ」を形にしたホームページづくりを大切にしています。「トップページに印象に残る動きを入れたい」「今のサイトをリニューアルしたい」といったご相談は、ホームページ制作をご覧のうえ、お問い合わせからお気軽にご相談ください。




