もみじ
背景エフェクト / 動く素材トップ
赤みの強い紅葉が、重なりながら降る。
試して調整する#
判断#
向く場面: 日本の秋。旅館・観光・催事。同じ紅葉でも赤みを強くしたいとき。
避ける場面: 長文の背面や、写真そのものを主役にする画面。
つまみは元の実装が持っている項目そのままです。実際に文字を重ねた状態で速さと密度を詰めます。
組み込み条件#
依存: 素のCanvas 2D。外部ライブラリは要らない。
負荷: 1画面に1つ。画面外・非表示タブ・動きを減らす設定で止まる。
再利用: コピーした単一HTMLがそのまま動く。
単一HTMLをコピーする#
下のコードだけで実物サンプルと同じ画面が動きます。調整欄を動かすと、値を反映したコードへ更新されます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,follow">
<title>もみじ 実物サンプル</title>
<style>
:root {
color-scheme: light dark;
--ink-light:#1a2421; --mut-light:#5a6a72; --signal-light:#2a7ab8; --bg-light:#f4f8fb;
--ink-dark:#f4f8fb; --mut-dark:#a8bac7; --signal-dark:#48b8ff; --bg-dark:#07111f;
}
* { box-sizing:border-box; }
html,body { margin:0; min-height:100%; }
body { font-family:"Hiragino Sans","Yu Gothic UI",system-ui,sans-serif; transition:background .3s,color .3s; }
html[data-demo-theme="light"], html[data-demo-theme="light"] body {
background:var(--bg-light); color:var(--ink-light); --ink:var(--ink-light); --mut:var(--mut-light); --signal:var(--signal-light); }
html[data-demo-theme="dark"], html[data-demo-theme="dark"] body {
background:var(--bg-dark); color:var(--ink-dark); --ink:var(--ink-dark); --mut:var(--mut-dark); --signal:var(--signal-dark); }
@media (prefers-color-scheme:light) {
html:not([data-demo-theme]), html:not([data-demo-theme]) body {
background:var(--bg-light); color:var(--ink-light); --ink:var(--ink-light); --mut:var(--mut-light); --signal:var(--signal-light); } }
@media (prefers-color-scheme:dark) {
html:not([data-demo-theme]), html:not([data-demo-theme]) body {
background:var(--bg-dark); color:var(--ink-dark); --ink:var(--ink-dark); --mut:var(--mut-dark); --signal:var(--signal-dark); } }
.sample { position:relative; min-height:100dvh; overflow:hidden; isolation:isolate; display:grid; align-items:end; }
.effect { position:absolute; inset:0; z-index:0; }
.effect canvas { display:block; width:100%; height:100%; touch-action:manipulation; }
.copy { position:relative; z-index:1; width:min(42rem,100%); padding:clamp(2rem,7vw,6rem); }
.label { margin:0 0 1rem; color:var(--signal); font:600 .72rem/1.4 ui-monospace,Consolas,monospace; letter-spacing:.16em; }
h1 { margin:0; max-width:14ch; font-size:clamp(2rem,5.5vw,4.5rem); line-height:1.05; letter-spacing:-.04em; }
.lead { margin:1.5rem 0 0; max-width:36rem; color:var(--mut); font-size:clamp(.95rem,1.4vw,1.1rem); line-height:1.85; }
.readout { position:absolute; inset:1.5rem 1.5rem auto auto; margin:0; color:var(--mut); font:500 .68rem/1.6 ui-monospace,Consolas,monospace; letter-spacing:.1em; writing-mode:vertical-rl; z-index:2; }
.theme-toggle { position:absolute; inset:1.5rem auto auto 1.5rem; z-index:2; display:flex; gap:.25rem; }
.theme-toggle button {
min-block-size:36px; padding-inline:.75rem; font:600 .72rem/1 ui-monospace,Consolas,monospace;
letter-spacing:.08em; border:1px solid var(--mut); background:transparent; color:var(--ink); cursor:pointer; border-radius:4px; }
.theme-toggle button[aria-pressed="true"] { background:var(--signal); color:var(--bg-dark); border-color:var(--signal); }
@media (max-width:640px) { .copy{padding:2rem 1.25rem 3rem}.readout{display:none} }
.bare .copy, .bare .readout, .bare .theme-toggle { display:none; }
</style>
</head>
<body>
<script id="effect-config" type="application/json">{"momiji-alpha":1.0,"momiji-count":40,"momiji-hue":15}</script>
<script>if (/[?&]bare=1/.test(location.search)) document.documentElement.classList.add('bare');
if (/[?&]tap=1/.test(location.search)) { addEventListener('load', function () {
setTimeout(function () { [[0.5,0.5],[0.3,0.6],[0.7,0.4]].forEach(function (p, i) {
setTimeout(function () { document.dispatchEvent(new MouseEvent('click',
{clientX: innerWidth * p[0], clientY: innerHeight * p[1], bubbles: true})); }, i * 220);
}); }, 300); }); }</script>
<script>(function(){var m=location.hash.match(/[#&]cfg=([^&]*)/);if(!m)return;
try{JSON.parse(decodeURIComponent(m[1]));document.getElementById('effect-config').textContent=decodeURIComponent(m[1]);}catch(e){}})();</script>
<script>(function(){
var root=document.documentElement, cfgNode=document.getElementById('effect-config');
var themeKeys=[];
function syncTheme(t){
root.setAttribute('data-demo-theme',t);
document.querySelectorAll('.theme-toggle button').forEach(function(b){
b.setAttribute('aria-pressed',b.dataset.theme===t?'true':'false');});
if(!themeKeys.length)return;
try{var cfg=JSON.parse(cfgNode.textContent||'{}');
themeKeys.forEach(function(k){cfg[k]=t==='dark'?0:1;});
cfgNode.textContent=JSON.stringify(cfg);}catch(e){}
}
var fixed=null;
if(fixed){syncTheme(fixed);}else{var q=location.search.match(/[?&]theme=(light|dark)/); if(q) syncTheme(q[1]);}
addEventListener('DOMContentLoaded',function(){
document.querySelectorAll('.theme-toggle button').forEach(function(b){
b.addEventListener('click',function(){syncTheme(b.dataset.theme);});});});})();</script>
<main class="sample">
<div class="theme-toggle" aria-label="配色切替">
<button type="button" data-theme="light" aria-pressed="false">LIGHT</button>
<button type="button" data-theme="dark" aria-pressed="false">DARK</button>
</div>
<div class="effect" aria-hidden="true"><canvas id="effect-canvas"></canvas></div>
<div class="copy">
<p class="label">背景エフェクト</p>
<h1>もみじ</h1>
<p class="lead">赤みの強い紅葉が、重なりながら降る。日本の秋。旅館・観光・催事。同じ紅葉でも赤みを強くしたいとき。</p>
</div>
<p class="readout">単一HTML・動きを減らす設定に対応</p>
</main>
<script>
/* ---- ここから下は AK²Engine の実装をそのまま持ってきたもの ------------- */
/**
* AK²-Engine / effects/momiji.js
* ================================
* 紅葉(もみじ)エフェクト(MomijiEffect)
*
* カスタムSVGパスから起こしたもみじ形を、
* フラットスピンと疑似3Dパースペクティブで舞わせる。
* 葉が「斜め上から見下ろされる」立体感を演出。
* 秋テーマ・和風・旅館・日本庭園系に。
*
* ── CSS 変数による設定 ─────────────────────────────
*
* --momiji-count
* 葉の数
* デフォルト: 40
* 推奨範囲: 15(静か)〜 70(賑やか)
* カードプレビュー用: 8〜12
*
* --momiji-alpha
* アルファ係数 0〜1
* デフォルト: 1.0(= 0.9 の標準値)
* 推奨範囲: 0.5〜1.0
*
* --momiji-hue
* 葉の基準色相(0〜360)
* デフォルト: 15(赤橙の紅葉色)
* 例: 5(深紅)/ 25(橙)/ 35(黄橙)
*
* 使い方:
* AK2.register(new MomijiEffect());
*/
class BgEffectMomiji {
/** @type {object} コンストラクタオプション(CSS変数より優先) */
#options = {};
/** @param {{{ count?: number, alpha?: number, hue?: number }}} options */
constructor(options = {}) {
this.#options = options;
}
/** @type {number} */
#w = 0;
/** @type {number} */
#h = 0;
/** @type {Array<object>} */
#particles = [];
/**
* 葉の数 — CSS 変数 --momiji-count で上書き可
* デフォルト: 40 | 推奨: 15〜70
* @type {number}
*/
#count = 40;
/**
* アルファ係数 — CSS 変数 --momiji-alpha で上書き可
* デフォルト: 1.0 | 推奨: 0.5〜1.0
* @type {number}
*/
#alphaScale = 1.0;
/**
* 基準色相 — CSS 変数 --momiji-hue で上書き可
* デフォルト: 15(赤橙の紅葉色)
* @type {number}
*/
#baseHue = 15;
// もみじ形状の頂点(SVGのviewBox=240x260、中心=120,130)
static #MAPLE_PATH = [
[120,220],[128,205],[143.6,170.2],[138.3,140.4],[156.2,164.2],
[197.8,182.4],[186.2,140.2],[167.7,115.5],[150.3,104],[168.2,104.2],
[197.8,91.8],[162.2,79.8],[144.2,85.8],[156.5,79.3],[162.1,56],
[138.7,67.8],[125.7,85.5],[125,35],[115,35],[114.3,85.5],
[102.3,68.3],[77.9,55.6],[83.9,80.2],[95.5,85.8],[77.9,79.6],
[41.8,91.5],[72,104.6],[90.1,104.3],[71.7,116.1],[53.7,140.1],
[41.9,182.2],[83.7,164.5],[101.3,140.3],[96.1,169.9],[112,205],
[120,220],
];
// ── AK²Engine インターフェイス ─────────────────
/**
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} _ctx
*/
init(canvas, _ctx) {
this.#w = canvas.width;
this.#h = canvas.height;
const style = getComputedStyle(document.documentElement);
const n = parseInt(style.getPropertyValue('--momiji-count'));
const a = parseFloat(style.getPropertyValue('--momiji-alpha'));
const h = parseFloat(style.getPropertyValue('--momiji-hue'));
if (n > 0) this.#count = n;
if (!isNaN(a)) this.#alphaScale = a;
if (!isNaN(h)) this.#baseHue = h;
// コンストラクタオプションは CSS 変数より優先(セクション個別指定に使用)
if (this.#options.count !== undefined) this.#count = this.#options.count;
if (this.#options.alpha !== undefined) this.#alphaScale = this.#options.alpha;
if (this.#options.hue !== undefined) this.#baseHue = this.#options.hue;
this.#particles = Array.from({ length: this.#count }, () => this.#reset({}, true));
}
onResize(w, h) {
this.#w = w;
this.#h = h;
}
/**
* @param {number} dt - 前フレームからの経過ミリ秒
*/
update(dt) {
const timeScale = dt / 6;
for (const p of this.#particles) {
// 1. 落下
p.y += (p.speedY + p.vy) * timeScale;
p.vy *= Math.pow(0.98, timeScale);
// 2. 左右の不規則な漂い
p.driftTimer -= timeScale;
if (p.driftTimer <= 0) {
p.driftTarget = (Math.random() - 0.5) * 1.5;
p.driftTimer = Math.random() * 150 + 50;
}
p.vx += (p.driftTarget - p.vx) * 0.01 * timeScale;
p.x += (p.speedX + p.vx) * timeScale;
// 3. 細かい揺らぎ
p.wobble += p.wobbleSpeed * timeScale;
p.x += Math.sin(p.wobble) * 0.05 * timeScale;
// 4. フラットスピン(平面回転)
p.angle += p.zSpinSpeed * timeScale;
// 5. 疑似3D(わずかな傾き変動)
p.flipPhase += p.flipSpeedX * timeScale;
p.flipPhaseY += p.flipSpeedY * timeScale;
// 画面外リセット
if (p.y > this.#h + p.size * 2 ||
p.x < -p.size * 2 ||
p.x > this.#w + p.size * 2) {
this.#reset(p, false);
}
}
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
draw(ctx) {
const path = BgEffectMomiji.#MAPLE_PATH;
const cx = 120;
const cy = 130;
for (const p of this.#particles) {
const flipScaleX = Math.cos(p.flipPhase) * 0.05 + 0.95;
const flipScaleY = Math.cos(p.flipPhaseY) * 0.05 + 0.95;
const drawAngle = p.angle + Math.sin(p.wobble) * 0.15;
const scale = p.size / 80;
ctx.save();
ctx.translate(p.x, p.y);
ctx.scale(1, 0.8); // 斜め上から見下ろす疑似パース
ctx.rotate(drawAngle);
ctx.scale(flipScaleX, flipScaleY);
ctx.fillStyle = `hsla(${p.hue}, 80%, 45%, ${p.alpha})`;
ctx.beginPath();
ctx.moveTo((path[0][0] - cx) * scale, (path[0][1] - cy) * scale);
for (let i = 1; i < path.length; i++) {
ctx.lineTo((path[i][0] - cx) * scale, (path[i][1] - cy) * scale);
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
// ── ユーティリティ ────────────────────────────────
/**
* @param {object} p
* @param {boolean} isFirstTime
*/
#reset(p, isFirstTime) {
p.x = Math.random() * this.#w;
p.y = isFirstTime ? Math.random() * this.#h : -50;
p.size = Math.random() * 6 + 10;
p.speedY = Math.random() * 0.2 + 0.6;
p.speedX = (Math.random() - 0.5) * 0.2;
p.vx = 0;
p.vy = 0;
p.driftTarget = 0;
p.driftTimer = Math.random() * 100;
p.wobble = Math.random() * Math.PI * 2;
p.wobbleSpeed = Math.random() * 0.02 + 0.01;
p.angle = Math.random() * Math.PI * 2;
const spinDir = Math.random() > 0.5 ? 1 : -1;
p.zSpinSpeed = spinDir * (Math.random() * 0.01 + 0.03);
p.flipPhase = Math.random() * Math.PI * 2;
p.flipSpeedX = Math.random() * 0.02 + 0.01;
p.flipPhaseY = Math.random() * Math.PI * 2;
p.flipSpeedY = Math.random() * 0.02 + 0.01;
p.hue = this.#baseHue + (Math.random() * 30 - 15);
p.alpha = 0.9 * this.#alphaScale;
return p;
}
}
// AK²Engine に登録
window.BgEffectMomiji = BgEffectMomiji;
/* ---- ここまで。以下は、この資料で回すための呼び出し ------------------- */
(function () {
var canvas = document.getElementById('effect-canvas');
var ctx = canvas.getContext('2d');
var cfgNode = document.getElementById('effect-config');
var reduced = matchMedia('(prefers-reduced-motion: reduce)');
var eff = null, raf = 0, last = 0, W = 1, H = 1, active = true;
var VARS = {"momiji-alpha": "--momiji-alpha", "momiji-count": "--momiji-count", "momiji-hue": "--momiji-hue"};
function applyVars(cfg) {
Object.keys(VARS).forEach(function (k) {
if (cfg[k] !== undefined && cfg[k] !== null) {
document.documentElement.style.setProperty(VARS[k], String(cfg[k]));
}
});
}
function measure() {
var box = canvas.getBoundingClientRect();
W = Math.max(1, Math.round(box.width));
H = Math.max(1, Math.round(box.height));
}
function build(cfg) {
applyVars(cfg);
measure();
canvas.width = W; canvas.height = H; /* init は canvas.width を読む */
eff = new BgEffectMomiji({});
if (eff.init) eff.init(canvas, ctx);
fit();
}
function fit() {
measure();
var dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (eff && eff.onResize) eff.onResize(W, H);
}
function paint(dt) {
/* 描く直前に実寸を見る。読み込みの順によっては、最初の計測で幅0が返る
(Browser ペインで 1x958 の buffer になった。ResizeObserver だけでは
戻らないことがあるので、毎フレーム安く確かめる) */
var box = canvas.getBoundingClientRect();
if (Math.abs(Math.round(box.width) - W) > 1 || Math.abs(Math.round(box.height) - H) > 1) {
fit();
}
ctx.clearRect(0, 0, W, H);
if (!eff) return;
if (eff.update) eff.update(dt);
if (eff.draw) eff.draw(ctx);
}
function report() {
document.documentElement.dataset.materialActive = String(active);
document.documentElement.dataset.materialRunning = String(raf !== 0);
}
function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; report(); }
function loop(time) {
if (!active || reduced.matches || document.hidden) { stop(); paint(16.67); return; }
var dt = last ? Math.min(60, time - last) : 16.67;
last = time;
paint(dt);
raf = requestAnimationFrame(loop);
report();
}
function start() {
stop(); last = 0;
if (active && !reduced.matches && !document.hidden) raf = requestAnimationFrame(loop);
else paint(16.67);
report();
}
addEventListener('message', function (e) {
var d = e.data || {};
if (d.type === 'material-config') {
cfgNode.textContent = JSON.stringify(d.values || {});
build(d.values || {});
start();
}
if (d.type === 'material-active') { active = d.active === true; start(); }
});
document.addEventListener('visibilitychange', start);
reduced.addEventListener('change', start);
new ResizeObserver(fit).observe(canvas);
addEventListener('pagehide', stop, {once: true});
build(JSON.parse(cfgNode.textContent));
start();
})();
</script>
</body>
</html>
AIエージェントへ渡す場合は、「このHTMLを対象ページのヒーロー背景へ組み込み、文字と主要ボタンを前面に残してください」と添えます。
組み込み時の確認#
prefers-reduced-motion: reduceで静止する- 画面外と非表示タブで描画が止まる
- 背景を止めても文字の階層と構図が成立する
- 文字とのコントラストを実際の配色で測る
- 装飾が主要操作の当たり判定を覆わない