01スクワークルカード — スーパー楕円指数の値が円と四角の間を流れ、カードの角の稜線が変わる方式
スーパー楕円指数の値が大きくなるほど角は四角形に近づき、1に近づくほど直線の切り取りに近づき、1枚のカードで曲率の性質が流れる。
使いどころアプリアイコンのプレビューカード、ギャラリーのサムネイルカード
SUPER_NSWEEP_MS$subject-blue

<main class="stage">
<div class="wrap is-demo" id="wrap">
<button class="card" id="card" type="button" aria-pressed="false" aria-label="曲率プリセットを切り替え"></button>
<p class="readout" id="readout" aria-live="polite">指数値 1.00</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 의 smootherstep(5차 다항식) 삼각파 보간에 있다 — 여기는 무대·판만 맡는다.
// corner-shape 는 실험적 속성이라 @supports 로 감지한다. 미지원 환경은 JS 가 border-radius 근사로 대신한다.
@include stage($stage-ink);
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.card {
appearance: none;
border: none;
width: 180px;
height: 112px;
background: $subject-blue;
border-radius: $r-card;
cursor: pointer;
transition: box-shadow $dur-quick $ease-out, transform $dur-instant $ease-out;
box-shadow: $shadow-raised;
&:hover { box-shadow: $shadow-press; }
&:active { transform: scale(.97); }
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 3px; }
}
@supports not (corner-shape: superellipse(1)) {
.card { border: 1px solid rgba($subject-cream, .3); }
}
.readout {
margin: 0;
color: $subject-cream;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.card { width: 140px; height: 92px; }
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
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;
}
.card {
appearance: none;
border: none;
width: 180px;
height: 112px;
background: #2f6df6;
border-radius: 16px;
cursor: pointer;
transition: box-shadow 220ms ease-out, transform 140ms ease-out;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.card:hover {
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.card:active {
transform: scale(0.97);
}
.card:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 3px;
}
@supports not (corner-shape: superellipse(1)) {
.card {
border: 1px solid rgba(255, 247, 230, 0.3);
}
}
.readout {
margin: 0;
color: #fff7e6;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.card {
width: 140px;
height: 92px;
}
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}(function () {
var SUPER_N = 4.0; // 슈퍼타원 지수 상한 — 흐름이 도달하는 사각형 쪽 끝값 (변형 01 목표)
var SWEEP_MS = 2400; // 원 → 사각형 → 원 한 바퀴 주기 (변형 03 목표)
var MIN_N = 1; // 슈퍼타원 지수 하한 — 원에 가장 가까운 끝값
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var SUPPORTS = CSS.supports('corner-shape', 'superellipse(1)');
var wrap = document.getElementById('wrap');
var card = document.getElementById('card');
var readout = document.getElementById('readout');
var isDemo = true;
var PRESETS = [MIN_N, (MIN_N + SUPER_N) / 2, SUPER_N];
var presetIdx = 0;
// 계산 겹: 스무더스텝(5차 다항식, Perlin) 으로 왕복 삼각파를 매끄러운 흐름으로 편다 — 선형 트윈이 아니다
function smootherstep(u) { return u * u * u * (u * (u * 6 - 15) + 10); }
function triangle(x) { var f = x - Math.floor(x); return f < 0.5 ? f * 2 : (1 - f) * 2; }
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
function setN(n) {
if (SUPPORTS) {
card.style.cornerShape = 'superellipse(' + n.toFixed(3) + ')';
card.style.borderRadius = '40px';
} else {
// 근사 폴백: 슈퍼타원 지수가 클수록 반경을 좁혀 각진 인상을 흉내(정확한 곡선은 아니다)
card.style.borderRadius = (40 / n).toFixed(2) + 'px';
}
readout.textContent = '指数値 ' + n.toFixed(2);
}
card.addEventListener('click', function () {
human();
presetIdx = (presetIdx + 1) % PRESETS.length;
card.setAttribute('aria-pressed', String(presetIdx > 0));
setN(PRESETS[presetIdx]);
});
var start = 0;
function loop(ts) {
if (!start) start = ts;
if (isDemo) {
if (reduce) {
setN(SUPER_N); // 계산은 유지, 시간 축소 대신 목표 상태로 즉시 고정
} else {
var u = triangle((ts - start) / SWEEP_MS);
setN(MIN_N + (SUPER_N - MIN_N) * smootherstep(u));
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// SquircleCard — vanilla 판(run/372/demo/01-corner-shape-squircle-card/index.html) 의 React 포트.
// 슈퍼타원 지수를 삼각파 → smootherstep(5차 다항식) 으로 편 흐름으로 순환시키고, corner-shape 미지원
// 환경에서는 JS 가 border-radius 근사로 대신한다.
import { useEffect, useRef } from "react";
import styles from "./SquircleCard.module.scss";
type Box<T> = { current: T };
const SUPER_N = 4.0; // 슈퍼타원 지수 상한 — 흐름이 도달하는 사각형 쪽 끝값 (변형 01 목표)
const SWEEP_MS = 2400; // 원 → 사각형 → 원 한 바퀴 주기 (변형 03 목표)
const MIN_N = 1; // 슈퍼타원 지수 하한 — 원에 가장 가까운 끝값
export default function SquircleCard() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const cardRef = useRef(null) as unknown as Box<HTMLButtonElement>;
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 card = cardRef.current;
const readout = readoutRef.current;
if (!root || !card || !readout) return;
let isDemo = true;
const PRESETS = [MIN_N, (MIN_N + SUPER_N) / 2, SUPER_N];
let presetIdx = 0;
// 계산 겹: 스무더스텝(5차 다항식) 으로 왕복 삼각파를 매끄러운 흐름으로 편다 — 선형 트윈이 아니다
function smootherstep(u: number) { return u * u * u * (u * (u * 6 - 15) + 10); }
function triangle(x: number) { const f = x - Math.floor(x); return f < 0.5 ? f * 2 : (1 - f) * 2; }
function human() { if (isDemo) { root!.classList.remove(styles.isDemo); isDemo = false; } }
function setN(n: number) {
if (SUPPORTS) {
(card!.style as any).cornerShape = "superellipse(" + n.toFixed(3) + ")";
card!.style.borderRadius = "40px";
} else {
card!.style.borderRadius = (40 / n).toFixed(2) + "px";
}
readout!.textContent = "지수 값 " + n.toFixed(2);
}
function onClick() {
human();
presetIdx = (presetIdx + 1) % PRESETS.length;
card!.setAttribute("aria-pressed", String(presetIdx > 0));
setN(PRESETS[presetIdx]);
}
card.addEventListener("click", onClick);
let start = 0;
function loop(ts: number) {
if (!start) start = ts;
if (isDemo) {
if (reduce) {
setN(SUPER_N);
} else {
const u = triangle((ts - start) / SWEEP_MS);
setN(MIN_N + (SUPER_N - MIN_N) * smootherstep(u));
}
}
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
card.removeEventListener("click", onClick);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<button
className={styles.card}
ref={cardRef}
type="button"
aria-pressed="false"
aria-label="곡률 프리셋 전환"
/>
<p className={styles.readout} ref={readoutRef} aria-live="polite">지수 값 1.00</p>
</div>
</main>
);
}01 スクワークルカード — スーパー楕円指数の値が円と四角の間を流れ、カードの角の稜線が変わる方式 — 計測 累積面積 1.36% · 動いたフレーム 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 の対応を起動時に この記事のデモをすべて見る


