Falling Petals Seasonal Backdrop (Switchable Wind)
A decorative backdrop for seasonal greeting pages. Each petal is modelled as a flat plate rather than a dot: its two in-plane axes are rotated Z→Y→X, projected to screen, and the resulting basis vectors are handed straight to ctx.transform. Because the determinant is the foreshortening, a petal collapses to a line when it turns edge-on and switches to its darker back-face colour when it flips — behaviour you cannot get by shrinking a width from an angle. Fall speed follows size: larger petals catch less wind and drop harder, so parallax emerges without hand-authored layers, and mixing the x coordinate into the sway phase keeps the flutter from looping. The state is reversible wind (three strengths plus a direction flip) rather than irreversible accumulation; transitions ease toward their target exponentially with frame time in the exponent, so petals decelerate and stream back when the wind reverses and the timing is identical on slow devices. Depth is split across two canvases, with the near layer composited above the text so petals cross in front of it. Eight sprites (front/back × four tones) are rendered once, synchronously, canvas-to-canvas. Under prefers-reduced-motion a single deterministic scattered frame is drawn and the wind buttons simply redraw it; the loop also stops off-screen and on hidden tabs. Zero dependencies, zero image assets.
- Added:
- 2026-09-27
- Dependencies:
- None
- tags
- #block #background #decoration #seasonal #canvas #particles #petals #wind #vertical-writing #motion #no-image #no-dependency
Preview
花のころ、いかがお過ごしですか
季節のたよりを、ひとひらに
ひらひらと、
向きを持って落ちる
一枚ずつが面の向きを持っているので、風に乗ると裏が見え、真横を向いた瞬間は線になります。 枚数を増やさなくても密度が出るのは、動きが「落ちる」だけで終わっていないからです。
- 舞う枚数
- 0枚
- 風速
- 0px/s
- 面が立つ
- 0枚
- 手前層
- 0枚
"use client";
import { useEffect, useRef, useState } from "react";
import { Zen_Old_Mincho } from "next/font/google";
// preload: false は和文フォント必須(unicode-range で約37分割されるため、preload すると
// 全ページに数十本の <link rel="preload"> がぶら下がる)。
const zenOldMincho = Zen_Old_Mincho({
weight: ["400"],
subsets: ["latin"],
display: "swap",
preload: false,
});
/** 花びらが舞い落ちるバックドロップ(BLK-82)。 */
type WindLevel = 0 | 1 | 2;
const WINDS: {
label: string;
note: string;
speed: number; // 水平方向の風速(px/s・基準幅900px時)
fall: number; // 落下の基準速度(px/s)
turbulence: number; // 縦揺れの強さ
tumble: number; // 回転の速さ倍率
density: number; // 枚数の倍率
}[] = [
{ label: "凪", note: "ほとんど風がない", speed: 26, fall: 34, turbulence: 0.35, tumble: 0.5, density: 0.6 },
{ label: "そよ", note: "標準の風", speed: 96, fall: 46, turbulence: 0.7, tumble: 1, density: 1 },
{ label: "花嵐", note: "ひとしきり強く吹く", speed: 226, fall: 62, turbulence: 1.25, tumble: 1.8, density: 1.5 },
];
/** 花びらの表面(淡い順)。白基調に馴染むアンバー寄りの4段 */
const FACE = ["#f9e2c0", "#f3cd99", "#e8ae6b", "#d18e3f"];
/** 裏面は1段暗く。裏返った瞬間に色が変わるので回転が読める */
const BACK = ["#eed2a6", "#e3b87e", "#d29551", "#b0722a"];
/** 輪郭と主脈 */
const EDGE = "rgba(160, 94, 18, 0.55)";
const SPRITE = 96; // スプライト1枚の論理サイズ
const REF_W = 900; // 風速の基準幅(狭い枠では比例して落とす)
const MAX_PETALS = 132;
/** 固定シード乱数(reduced-motion の静止画を毎回同じ絵にする) */
function mulberry32(seed: number) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** 1枚の花びら(表・裏)を論理サイズ SPRITE の canvas へ描く */
function makeSprite(fill: string, stroke: string): HTMLCanvasElement {
const dpr = Math.min(typeof window === "undefined" ? 1 : window.devicePixelRatio || 1, 2);
const c = document.createElement("canvas");
c.width = SPRITE * dpr;
c.height = SPRITE * dpr;
const g = c.getContext("2d");
if (!g) return c;
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const cx = SPRITE / 2;
// しずく型:先端を細く、付け根を丸く。先端には浅い切れ込みを入れる
g.beginPath();
g.moveTo(cx, SPRITE * 0.1);
g.bezierCurveTo(SPRITE * 0.14, SPRITE * 0.3, SPRITE * 0.16, SPRITE * 0.78, cx, SPRITE * 0.92);
g.bezierCurveTo(SPRITE * 0.84, SPRITE * 0.78, SPRITE * 0.86, SPRITE * 0.3, cx, SPRITE * 0.1);
g.closePath();
g.fillStyle = fill;
g.fill();
g.strokeStyle = stroke;
g.lineWidth = 1.1;
g.stroke();
// 主脈。1本だけ入れると回転したとき面の向きが読める
g.beginPath();
g.moveTo(cx, SPRITE * 0.16);
g.lineTo(cx, SPRITE * 0.86);
g.strokeStyle = stroke;
g.lineWidth = 0.9;
g.stroke();
return c;
}
type Petal = {
x: number;
y: number;
z: number; // 0=奥 / 1=手前
size: number;
tone: number; // 濃淡(0..3)
wind: number; // 風の受けやすさ(小さい花びらほど大きい)
vx: number;
vy: number;
wave: number; // 縦揺れの位相
ax: number;
ay: number;
az: number;
sx: number; // 前後の傾きの速さ
sz: number; // 面内の回転の速さ
free: boolean; // true=水平軸まわりに回り続ける(=裏返る)少数派
sy: number; // free のときの回転速度
wob: number; // free でないときの傾きの振れ幅(rad)
fq: number; // 振れの周期(Hz)
ph: number; // 振れの位相
};
export default function PetalFallSeasonalBackdrop() {
const [wind, setWind] = useState<WindLevel>(1);
const [dir, setDir] = useState<1 | -1>(-1);
const [still, setStill] = useState(false);
const [inView, setInView] = useState(false);
const [stats, setStats] = useState({ petals: 0, speed: 0, edge: 0, near: 0 });
const stageRef = useRef<HTMLDivElement>(null);
const backRef = useRef<HTMLCanvasElement>(null);
const frontRef = useRef<HTMLCanvasElement>(null);
const windRef = useRef<WindLevel>(1);
const dirRef = useRef<1 | -1>(-1);
const apiRef = useRef<{ refresh: () => void } | null>(null);
useEffect(() => {
const id = requestAnimationFrame(() => setInView(true));
return () => cancelAnimationFrame(id);
}, []);
// 風の設定はシミュレーション側が毎フレーム読むので ref へ。静止時は1枚を描き直す
useEffect(() => {
windRef.current = wind;
dirRef.current = dir;
apiRef.current?.refresh();
}, [wind, dir]);
useEffect(() => {
const stage = stageRef.current;
const back = backRef.current;
const front = frontRef.current;
if (!stage || !back || !front) return;
const bctx = back.getContext("2d");
const fctx = front.getContext("2d");
if (!bctx || !fctx) return;
const faces = FACE.map((f) => makeSprite(f, EDGE));
const backs = BACK.map((f) => makeSprite(f, EDGE));
let w = 1;
let h = 1;
let ready = false;
let petals: Petal[] = [];
let raf = 0;
let last = 0;
let statAt = 0;
// 風は目標値へ指数で寄せる(dt を指数に入れるので fps が落ちても到達時間は同じ)
let speedNow = WINDS[1].speed;
let fallNow = WINDS[1].fall;
let turbNow = WINDS[1].turbulence;
let tumbleNow = WINDS[1].tumble;
let dirNow = -1; // -1..1 の連続値。符号が反転するまで数百msかかる
const scale = () => Math.min(1, Math.max(0.45, w / REF_W));
const spawn = (onScreen: boolean, rnd: () => number): Petal => {
const s = scale();
const z = rnd();
const size = (15 + rnd() * 26) * (0.62 + z * 0.55) * (0.7 + s * 0.3);
const d = dirRef.current;
return {
x: onScreen ? rnd() * w : d === -1 ? w + size + rnd() * w * 0.8 : -size - rnd() * w * 0.8,
// 半分は上から、半分は風上の側面から入れる。上からだけにすると風下の下半分にだけ
// 溜まって見え、枠の上半分が空いてしまう
y: onScreen ? rnd() * h : rnd() < 0.5 ? -size - rnd() * h * 0.4 : rnd() * h * 0.75 - size,
z,
size,
tone: Math.min(3, Math.floor(rnd() * 4)),
// 大きい(手前の)花びらは風を受けにくく、重く落ちる=勝手に視差になる
wind: Math.max(0.22, Math.min(1, 1.12 - z * 0.62 - rnd() * 0.22)),
vx: 0,
vy: 0,
wave: rnd() * Math.PI * 2,
ax: (rnd() - 0.5) * 0.8,
ay: 0,
az: rnd() * Math.PI * 2,
sx: (rnd() - 0.5) * 0.5,
sz: (rnd() - 0.5) * 0.9,
// 3枚に1枚だけを回り続ける役にする。全部が回ると常時どれかが真横を向き、
// 花びらの形が読めない絵になる(裏返りは「たまに起きる」から効く)
free: rnd() < 0.34,
sy: (rnd() - 0.5) * 1.8,
wob: 0.55 + rnd() * 0.6,
fq: 0.22 + rnd() * 0.35,
ph: rnd() * Math.PI * 2,
};
};
const targetCount = () =>
Math.round(
Math.min(MAX_PETALS, Math.max(16, ((w * h) / 8200) * WINDS[windRef.current].density)),
);
/** 1枚を描く。戻り値は面の見え方(0=面が立っている / 1=真正面) */
const paint = (ctx: CanvasRenderingContext2D, p: Petal) => {
const cz = Math.cos(p.az);
const sz = Math.sin(p.az);
const cy = Math.cos(p.ay);
const sy = Math.sin(p.ay);
const cx = Math.cos(p.ax);
const sxx = Math.sin(p.ax);
// 面内2軸を Z→Y→X の順で回してから画面へ射影する
const ux = cz * cy;
const uy = sz * cx + cz * sy * sxx;
const vx = -sz * cy;
const vy = cz * cx - sz * sy * sxx;
const det = ux * vy - uy * vx;
ctx.globalAlpha = 0.55 + p.z * 0.42;
if (Math.abs(det) < 0.05) {
// 面が真横を向いた瞬間。drawImage では消えるので、板の稜線を1本引いて残す
const len = p.size * 0.5;
ctx.beginPath();
ctx.moveTo(p.x - vx * len, p.y - vy * len);
ctx.lineTo(p.x + vx * len, p.y + vy * len);
ctx.strokeStyle = EDGE;
ctx.lineWidth = Math.max(1, p.size * 0.06);
ctx.stroke();
return det;
}
const sprite = det < 0 ? backs[p.tone] : faces[p.tone];
ctx.save();
ctx.translate(p.x, p.y);
ctx.transform(ux, uy, vx, vy, 0, 0);
ctx.drawImage(sprite, -p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
return det;
};
const clear = () => {
bctx.clearRect(0, 0, w, h);
fctx.clearRect(0, 0, w, h);
};
/** 手前層だけ前面 canvas へ。1本の配列を2パスで振り分けるだけ */
const drawAll = () => {
clear();
let edge = 0;
let near = 0;
for (const p of petals) {
const front = p.z > 0.72;
const det = paint(front ? fctx : bctx, p);
if (Math.abs(det) < 0.18) edge += 1;
if (front) near += 1;
}
bctx.globalAlpha = 1;
fctx.globalAlpha = 1;
return { edge, near };
};
const step = (dt: number) => {
const cfg = WINDS[windRef.current];
const s = scale();
const k = 1 - Math.exp(-dt / 520); // 風の立ち上がり・鎮まり
speedNow += (cfg.speed * s - speedNow) * k;
fallNow += (cfg.fall * s - fallNow) * k;
turbNow += (cfg.turbulence - turbNow) * k;
tumbleNow += (cfg.tumble - tumbleNow) * k;
dirNow += (dirRef.current - dirNow) * (1 - Math.exp(-dt / 700));
// 枚数の増減も1フレーム2枚までにして、段を切り替えた瞬間に湧かせない
const target = targetCount();
const rnd = Math.random;
if (petals.length < target) {
for (let i = 0; i < Math.min(2, target - petals.length); i += 1) petals.push(spawn(false, rnd));
} else if (petals.length > target) {
petals.length = Math.max(target, petals.length - 2);
}
const sec = dt / 1000;
const margin = 140;
for (const p of petals) {
const targetVx = speedNow * p.wind * dirNow;
p.vx += (targetVx - p.vx) * (1 - Math.exp(-dt / 320));
p.x += p.vx * sec;
// 落下は「重さ(風を受けにくさ)」で決まる。横揺れの位相に x を混ぜると
// 風に流れるほど揺れが進み、周期が噛み合わない
const heavy = 1.5 - p.wind;
const wave = Math.sin(p.x * 0.012 + p.wave);
const targetVy = fallNow * heavy + wave * turbNow * 26;
p.vy += (targetVy - p.vy) * (1 - Math.exp(-dt / 420));
p.y += p.vy * sec;
const spin = tumbleNow * sec;
p.ax += p.sx * spin;
p.az += p.sz * spin;
if (p.free) {
p.ay += (p.sy + p.vx * 0.004) * spin;
} else {
// 回り続けずに振れるだけの多数派。面はほぼ正面を向いたままひらひらする
p.ph += p.fq * tumbleNow * sec * Math.PI * 2;
p.ay = Math.sin(p.ph) * p.wob;
}
const outX = p.x < -margin || p.x > w + margin;
if (outX || p.y > h + margin) Object.assign(p, spawn(false, rnd));
}
};
/** reduced-motion 用。固定シードで散らした1枚(風の段で密度と傾きが変わる) */
const drawStatic = () => {
if (!ready) return;
const cfg = WINDS[windRef.current];
const rnd = mulberry32(20260927);
const count = targetCount();
petals = [];
for (let i = 0; i < count; i += 1) {
const p = spawn(true, rnd);
p.x = rnd() * (w + 80) - 40;
p.y = rnd() * (h + 80) - 40;
// 風が強いほど流れの向きへ傾いた姿勢で止める(静止画でも風向が読める)
p.az = dirRef.current * (0.25 + cfg.tumble * 0.5) + (rnd() - 0.5) * 0.9;
// 大半は正面寄り、1割ほどが真横〜裏面。動いている絵の分布に合わせる
p.ay = rnd() < 0.12 ? (rnd() - 0.5) * 4.4 : (rnd() - 0.5) * 1.7;
p.ax = (rnd() - 0.5) * 1.1;
petals.push(p);
}
const { edge, near } = drawAll();
setStats({ petals: count, speed: Math.round(cfg.speed * scale()), edge, near });
};
const frame = (t: number) => {
raf = requestAnimationFrame(frame);
if (!ready) return;
const dt = last ? Math.min(64, t - last) : 16;
last = t;
step(dt);
const { edge, near } = drawAll();
if (t - statAt > 280) {
statAt = t;
setStats({ petals: petals.length, speed: Math.round(Math.abs(speedNow * dirNow)), edge, near });
}
};
const start = () => {
if (raf || mql.matches || !visible || !onScreen) return;
last = 0;
raf = requestAnimationFrame(frame);
};
const stop = () => {
if (!raf) return;
cancelAnimationFrame(raf);
raf = 0;
};
let visible = document.visibilityState !== "hidden";
let onScreen = false;
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const resize = () => {
// 縮小プレビューの中でも寸法が合うよう、変形後の矩形ではなくレイアウト寸法を採る
if (!stage.clientWidth || !stage.clientHeight) return;
w = stage.clientWidth;
h = stage.clientHeight;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
for (const c of [back, front]) {
c.width = Math.round(w * dpr);
c.height = Math.round(h * dpr);
const ctx = c.getContext("2d");
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const first = !ready;
ready = true;
if (mql.matches) {
drawStatic();
return;
}
if (first) {
const rnd = Math.random;
petals = Array.from({ length: targetCount() }, () => spawn(true, rnd));
}
// canvas の width/height 代入は中身を消す。rAF が止まっている(画面外・タブ非表示)
// 間に寸法が変わると空白のまま残るので、リサイズのたびに必ず1枚描き直す
const { edge, near } = drawAll();
setStats({ petals: petals.length, speed: Math.round(Math.abs(speedNow)), edge, near });
};
const ro = new ResizeObserver(resize);
ro.observe(stage);
resize();
const io = new IntersectionObserver(
(entries) => {
onScreen = entries.some((e) => e.isIntersecting);
if (onScreen) start();
else stop();
},
{ rootMargin: "120px" },
);
io.observe(stage);
const onVisibility = () => {
visible = document.visibilityState !== "hidden";
if (visible) start();
else stop();
};
const onMotionChange = () => {
setStill(mql.matches);
if (mql.matches) {
stop();
drawStatic();
} else start();
};
// 画面外で風を変えても値が古びないよう、静止時はここで描き直す
apiRef.current = {
refresh: () => {
if (mql.matches) drawStatic();
},
};
document.addEventListener("visibilitychange", onVisibility);
mql.addEventListener("change", onMotionChange);
setStill(mql.matches);
if (mql.matches) drawStatic();
return () => {
stop();
apiRef.current = null;
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", onVisibility);
mql.removeEventListener("change", onMotionChange);
};
}, []);
const revealClass = `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
inView
? "translate-y-0 opacity-100"
: "translate-y-3 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
}`;
// 縦組みは行を配列で持ち flex-row-reverse で並べる(折り返しをブラウザに任せない)
const greeting = ["花のころ、いかがお過ごしですか", "季節のたよりを、ひとひらに"];
const readouts = [
{ label: "舞う枚数", value: stats.petals.toLocaleString("en-US"), unit: "枚" },
{ label: "風速", value: `${stats.speed}`, unit: "px/s" },
{ label: "面が立つ", value: `${stats.edge}`, unit: "枚" },
{ label: "手前層", value: `${stats.near}`, unit: "枚" },
];
return (
<div className="w-full max-w-4xl overflow-hidden rounded-2xl border border-gray-200 bg-white">
{/* 操作卓。堆積のような不可逆な状態は持たず、風の強さと向きだけを切り替える */}
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 border-b border-gray-200 px-4 py-3 sm:px-7 sm:py-4">
<div className="flex items-baseline gap-3">
<span className="font-mono text-[11px] font-bold tracking-[0.12em] text-amber-600">
{WINDS[wind].label}
</span>
<span className="text-[11px] tracking-[0.04em] text-gray-500">{WINDS[wind].note}</span>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto">
<div className="flex border border-gray-200">
{WINDS.map((wd, i) => (
<button
key={wd.label}
type="button"
aria-pressed={wind === i}
onClick={() => setWind(i as WindLevel)}
className={`border-l border-gray-200 px-3.5 py-1.5 text-[12px] font-bold transition-colors first:border-l-0 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-amber-600 motion-reduce:transition-none ${
wind === i ? "bg-gray-900 text-white" : "text-gray-500 hover:text-gray-900"
}`}
>
{wd.label}
</button>
))}
</div>
<button
type="button"
onClick={() => setDir((d) => (d === -1 ? 1 : -1))}
className="border border-gray-300 px-4 py-1.5 text-[12px] font-bold text-gray-600 transition-colors hover:border-gray-900 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
風向を変える({dir === -1 ? "右から左へ" : "左から右へ"})
</button>
</div>
</div>
<div ref={stageRef} className="relative min-h-[400px] overflow-hidden sm:min-h-[470px]">
{/* 奥の層。版面の背面に置く */}
<canvas ref={backRef} aria-hidden="true" className="absolute inset-0 block h-full w-full" />
<div className="relative flex flex-col gap-8 px-5 py-9 sm:flex-row sm:items-start sm:gap-10 sm:px-10 sm:py-12">
{/* 縦組みの季節の挨拶。明朝・行は固定 */}
<div
className={`${revealClass} ${zenOldMincho.className} order-1 flex flex-row-reverse justify-end gap-3 self-end sm:order-2 sm:ml-auto sm:self-start`}
>
{greeting.map((line, i) => (
<p
key={line}
style={{ transitionDelay: inView ? `${120 + i * 140}ms` : "0ms" }}
className={`text-[15px] leading-[2.1] tracking-[0.12em] text-gray-900 [writing-mode:vertical-rl] sm:text-[17px] ${
i === 1 ? "pt-7 text-gray-600" : ""
} ${revealClass}`}
>
{line}
</p>
))}
</div>
{/* 本文。背面が白なのでパネルを敷かず、罫線と余白だけでまとめる */}
<div className={`${revealClass} order-2 max-w-sm sm:order-1`}>
<div className="flex items-center gap-3">
<span aria-hidden="true" className="h-px w-8 bg-amber-600" />
<span className="text-[11px] font-bold tracking-[0.1em] text-amber-600">
季節のごあいさつに添える背景
</span>
</div>
<h3 className="mt-4 text-[clamp(1.3rem,4.4vw,1.95rem)] leading-[1.5] font-bold tracking-[0.01em] text-gray-900">
ひらひらと、
<br />
向きを持って落ちる
</h3>
<p className="mt-4 bg-white/85 text-[12.5px] leading-[1.95] text-gray-600 sm:text-[13.5px]">
一枚ずつが面の向きを持っているので、風に乗ると裏が見え、真横を向いた瞬間は線になります。
枚数を増やさなくても密度が出るのは、動きが「落ちる」だけで終わっていないからです。
</p>
<div className="mt-6 flex flex-col items-start gap-2 border-t border-gray-200 pt-4">
<a
href="#"
className="inline-block border-b border-amber-600 pb-1 text-[12.5px] font-bold text-gray-900 transition-colors hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
季節の挨拶ページを相談する
</a>
<span className="text-[11px] tracking-[0.04em] text-gray-400">
{still ? "モーション低減中は散った一枚を静止表示" : "風の強さと向きは上のボタンで切り替わります"}
</span>
</div>
</div>
</div>
{/* 手前の層。版面より上に重なり、本文の前を横切る */}
<canvas
ref={frontRef}
aria-hidden="true"
className="pointer-events-none absolute inset-0 block h-full w-full"
/>
</div>
{/* 計測帯。カードは使わず hairline 罫線で区切った等幅の数値だけ並べる */}
<dl className="grid grid-cols-2 border-t border-gray-200 sm:grid-cols-4">
{readouts.map((r, i) => (
<div
key={r.label}
className={`flex items-baseline justify-between gap-2 px-4 py-3 sm:px-7 sm:py-4 ${
i % 2 === 1 ? "border-l border-gray-200" : ""
} ${i >= 2 ? "border-t border-gray-200 sm:border-t-0" : ""} ${
i === 2 ? "sm:border-l sm:border-gray-200" : ""
}`}
>
<dt className="text-[11px] tracking-[0.08em] text-gray-500">{r.label}</dt>
<dd className="font-mono text-[13px] font-bold text-gray-900 tabular-nums">
{r.value}
<span className="ml-0.5 text-[10px] font-medium text-gray-400">{r.unit}</span>
</dd>
</div>
))}
</dl>
</div>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/petal-fall-seasonal-backdrop.json