08フォールバック並び比較 — 同じ半径値を与えた対応ペインとフォールバックペインの輪郭を並べて比べる方式
同じ半径値を与えた対応ペインとフォールバックペインを並べ、どの角まで同じでどこから分かれるかを示す。
使いどころブラウザ対応の案内画面、互換性表
R_MAXSWEEP_MS$subject-lilac

<main class="stage">
<div class="wrap is-demo" id="wrap">
<div class="row">
<figure class="cell">
<div class="box native" id="nativeBox"></div>
<figcaption class="cap" id="capNative">対応環境</figcaption>
</figure>
<figure class="cell">
<div class="box shim" id="shimBox"></div>
<figcaption class="cap">非対応フォールバック</figcaption>
</figure>
</div>
<p class="readout" id="readout" aria-live="polite">現在の半径 20.0</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 에 있다 — 코사인 왕복으로 두 박스의 반경을 같은 값으로 움직여 차이를 겨룬다.
@include stage($stage-paper);
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.row {
display: flex;
gap: $sp-6;
}
.cell {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.box {
width: 120px;
height: 92px;
background: $subject-lilac;
}
.native {
background: $subject-blue-deep;
}
.cap {
font-size: 12px;
font-weight: 700;
color: $subject-ink;
}
.readout {
margin: 0;
color: $subject-ink;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.box { width: 96px; height: 76px; }
.row { gap: $sp-4; }
}
@media (prefers-reduced-motion: reduce) {
.box { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.row {
display: flex;
gap: 24px;
}
.cell {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.box {
width: 120px;
height: 92px;
background: #b9a0ff;
}
.native {
background: #0a4fe8;
}
.cap {
font-size: 12px;
font-weight: 700;
color: #17141a;
}
.readout {
margin: 0;
color: #17141a;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.box {
width: 96px;
height: 76px;
}
.row {
gap: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.box {
transition: none;
}
}(function () {
var R_MIN = 6; // 半径 하한(px)
var R_MAX = 34; // 半径 상한(px)
var SWEEP_MS = 2000; // 반경이 하한↔상한을 왕복하는 주기
var SUPER_N = 2.6; // 슈퍼타원 지수 고정값 — 원과 사각형 사이
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var SUPPORTS = CSS.supports('corner-shape', 'superellipse(1)');
var wrap = document.getElementById('wrap');
var nativeBox = document.getElementById('nativeBox');
var shimBox = document.getElementById('shimBox');
var capNative = document.getElementById('capNative');
var readout = document.getElementById('readout');
var isDemo = true;
var paused = false;
var frozenT = 0;
// 캡션을 실제 지원 여부로 갱신 — 미지원 환경에서 열면 두 칸 다 폴백으로 표시된다
if (!SUPPORTS) capNative.textContent = '非対応フォールバック';
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
// 계산 겹: 반경을 코사인으로 왕복시킨다 — 사인이 아니라 위상을 맞춘 코사인으로 0초에 하한에서 시작한다
function radiusAt(t) {
var c = (1 - Math.cos(t * Math.PI * 2)) / 2;
return R_MIN + (R_MAX - R_MIN) * c;
}
function applyBoth(r) {
if (SUPPORTS) {
nativeBox.style.cornerShape = 'superellipse(' + SUPER_N.toFixed(1) + ')';
nativeBox.style.borderRadius = r.toFixed(2) + 'px';
} else {
nativeBox.style.borderRadius = r.toFixed(2) + 'px';
}
// 오른쪽 칸은 언제나 border-radius 근사만 쓴다 — 두 렌더링의 차이를 나란히 겨룬다
shimBox.style.borderRadius = r.toFixed(2) + 'px';
readout.textContent = '現在の半径 ' + r.toFixed(1);
}
wrap.addEventListener('click', function () {
human();
paused = !paused;
if (paused) frozenT = performance.now();
});
wrap.addEventListener('keydown', function (e) {
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); wrap.click(); }
});
var start = 0;
function loop(ts) {
if (!start) start = ts;
var t;
if (reduce) {
t = 0.5; // 상태(상한)에 고정 — 코사인 계산은 그대로 쓴다
} else if (paused) {
t = ((frozenT - start) % SWEEP_MS) / SWEEP_MS;
} else {
t = ((ts - start) % SWEEP_MS) / SWEEP_MS;
}
applyBoth(radiusAt(t));
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// FallbackPolyfill — vanilla 판(run/372/demo/08-corner-shape-fallback-polyfill/index.html) 의 React 포트.
// 반경을 코사인으로 왕복시킨다 — 사인이 아니라 위상을 맞춘 코사인으로 0초에 하한에서 시작한다.
import { useEffect, useRef } from "react";
import styles from "./FallbackPolyfill.module.scss";
type Box<T> = { current: T };
const R_MIN = 6; // 반경 하한(px)
const R_MAX = 34; // 반경 상한(px)
const SWEEP_MS = 2000; // 반경이 하한↔상한을 왕복하는 주기
const SUPER_N = 2.6; // 슈퍼타원 지수 고정값 — 원과 사각형 사이
export default function FallbackPolyfill() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const nativeRef = useRef(null) as unknown as Box<HTMLDivElement>;
const shimRef = useRef(null) as unknown as Box<HTMLDivElement>;
const capRef = useRef(null) as unknown as Box<HTMLElement>;
const readoutRef = useRef(null) as unknown as Box<HTMLParagraphElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SUPPORTS = CSS.supports("corner-shape", "superellipse(1)");
const root = rootRef.current;
const nativeBox = nativeRef.current;
const shimBox = shimRef.current;
const capNative = capRef.current;
const readout = readoutRef.current;
if (!root || !nativeBox || !shimBox || !capNative || !readout) return;
let isDemo = true;
let paused = false;
let frozenT = 0;
// 캡션을 실제 지원 여부로 갱신 — 미지원 환경에서 열면 두 칸 다 폴백으로 표시된다
if (!SUPPORTS) capNative.textContent = "미지원 폴백";
function human() { if (isDemo) { root!.classList.remove(styles.isDemo); isDemo = false; } }
// 계산 겹: 반경을 코사인으로 왕복시킨다 — 사인이 아니라 위상을 맞춘 코사인으로 0초에 하한에서 시작한다
function radiusAt(t: number) {
const c = (1 - Math.cos(t * Math.PI * 2)) / 2;
return R_MIN + (R_MAX - R_MIN) * c;
}
function applyBoth(r: number) {
if (SUPPORTS) {
(nativeBox!.style as any).cornerShape = "superellipse(" + SUPER_N.toFixed(1) + ")";
}
nativeBox!.style.borderRadius = r.toFixed(2) + "px";
shimBox!.style.borderRadius = r.toFixed(2) + "px";
readout!.textContent = "지금 반경 " + r.toFixed(1);
}
function onClick() {
human();
paused = !paused;
if (paused) frozenT = performance.now();
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === " " || e.key === "Enter") { e.preventDefault(); onClick(); }
}
root.addEventListener("click", onClick);
root.addEventListener("keydown", onKeyDown);
let start = 0;
function loop(ts: number) {
if (!start) start = ts;
let t: number;
if (reduce) {
t = 0.5;
} else if (paused) {
t = ((frozenT - start) % SWEEP_MS) / SWEEP_MS;
} else {
t = ((ts - start) % SWEEP_MS) / SWEEP_MS;
}
applyBoth(radiusAt(t));
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
root.removeEventListener("click", onClick);
root.removeEventListener("keydown", onKeyDown);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef} tabIndex={0}>
<div className={styles.row}>
<figure className={styles.cell}>
<div className={styles.box + " " + styles.native} ref={nativeRef} />
<figcaption className={styles.cap} ref={capRef}>지원 환경</figcaption>
</figure>
<figure className={styles.cell}>
<div className={styles.box + " " + styles.shim} ref={shimRef} />
<figcaption className={styles.cap}>미지원 폴백</figcaption>
</figure>
</div>
<p className={styles.readout} ref={readoutRef} aria-live="polite">지금 반경 20.0</p>
</div>
</main>
);
}08 フォールバック並び比較 — 同じ半径値を与えた対応ペインとフォールバックペインの輪郭を並べて比べる方式 — 計測 累積面積 1.415% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · css corner-shape 9選 — border-radiusで切らない角の処理
このデモが属する記事
css corner-shape 9選 — border-radiusで切らない角の処理9つのコーナー形状のうち、1つはスーパー楕円指数の値が円と四角の間を流れ、1つはsquircle比率の値でカードの角が補間され、1つはベベル角度の値が増すほど面の幅が広がります。この9つは、css corner-shape の対応を起動時に この記事のデモをすべて見る


