08패턴 인식 값 전환 — 앞자리 숫자 값이 정해진 패턴 값과 맞으면 옆의 도형 값이 다른 모양으로 바뀌는 방식
앞자리 숫자 값이 정해진 패턴 값과 맞아떨어지면 옆의 도형 값이 부드럽게 다른 모양으로 바뀝니다.
쓰임카드 번호로 카드 종류를 구분해 보여주는 폼, 결제 폼의 실시간 판별 표시
MORPH_MSPREFIX_MAPSHAPES
<main class="stage">
<div class="cn is-demo" id="cn">
<label class="cn__field">
<input class="cn__input" id="cnInput" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" maxlength="16" value="">
</label>
<div class="cn__shapeBox">
<svg viewBox="0 0 40 40" width="34" height="34" aria-hidden="true">
<polygon class="cn__poly" id="cnPoly" points="20,10 28,15 28,25 20,30 12,25 12,15"/>
</svg>
<span class="cn__tag" id="cnTag">패턴 미인식</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 앞자리 패턴 값 → 가상 도형 A/B/C 점 배열을 rAF 로 보간한다(index.html, 6점 폴리곤).
@include stage($stage-paper);
.cn {
width: 100%;
max-width: 300px;
display: flex;
align-items: center;
gap: $sp-4;
}
.cn__field { flex: 1; display: block; }
.cn__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: $subject-ink;
color: $subject-cream;
font-family: "SFMono-Regular", Consolas, monospace;
font-weight: 600;
font-size: 15px;
letter-spacing: .04em;
&:focus { outline: 2px solid $subject-ink; outline-offset: 2px; }
}
.cn__shapeBox {
display: grid;
justify-items: center;
gap: $sp-1;
width: 68px;
}
.cn__poly { fill: $subject-blue-deep; }
.cn__tag {
font-weight: 700;
font-size: 10px;
color: rgba($subject-ink, .72);
white-space: nowrap;
}
@media (max-width: 340px) {
.cn { gap: $sp-2; }
.cn__input { padding: $sp-2 $sp-3; font-size: 13px; }
.cn__shapeBox { width: 56px; }
}
@media (prefers-reduced-motion: reduce) {
// 도형 보간은 JS rAF 로 계산되므로 reduce 시 매 프레임 목표값으로 즉시 스냅한다(index.html MORPH_MS 분기 없음 — 코멘트 유지용)
.cn__poly { 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);
}
.cn {
width: 100%;
max-width: 300px;
display: flex;
align-items: center;
gap: 16px;
}
.cn__field {
flex: 1;
display: block;
}
.cn__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: #17141a;
color: #fff7e6;
font-family: "SFMono-Regular", Consolas, monospace;
font-weight: 600;
font-size: 15px;
letter-spacing: 0.04em;
}
.cn__input:focus {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.cn__shapeBox {
display: grid;
justify-items: center;
gap: 4px;
width: 68px;
}
.cn__poly {
fill: #0a4fe8;
}
.cn__tag {
font-weight: 700;
font-size: 10px;
color: rgba(23, 20, 26, 0.72);
white-space: nowrap;
}
@media (max-width: 340px) {
.cn {
gap: 8px;
}
.cn__input {
padding: 8px 12px;
font-size: 13px;
}
.cn__shapeBox {
width: 56px;
}
}
@media (prefers-reduced-motion: reduce) {
.cn__poly {
transition: none;
}
}(function () {
var root = document.getElementById('cn');
var input = document.getElementById('cnInput');
var poly = document.getElementById('cnPoly');
var tag = document.getElementById('cnTag');
var MORPH_MS = 420; // 계산 겹: 앞자리 패턴 값이 바뀔 때 도형 점을 이만큼 동안 보간한다
// 가상 도형 3종(A/B/C) — 실제 카드사 로고·BIN 범위와 무관한 중립 라벨.
var SHAPES = {
A: [[20, 4], [33.9, 12], [33.9, 28], [20, 36], [6.1, 28], [6.1, 12]],
B: [[29, 4.4], [33, 20], [29, 35.6], [13.5, 31.3], [2, 20], [13.5, 8.7]],
C: [[20, 1], [26.9, 16], [36.5, 29.5], [20, 28], [3.5, 29.5], [13.1, 16]],
N: [[20, 10], [28, 15], [28, 25], [20, 30], [12, 25], [12, 15]]
};
var PREFIX_MAP = { '3': 'C', '4': 'A', '5': 'B' };
var current = SHAPES.N.map(function (p) { return p.slice(); });
var raf = null;
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function morphTo(key) {
var target = SHAPES[key];
var start = current.map(function (p) { return p.slice(); });
var t0 = performance.now();
if (raf) cancelAnimationFrame(raf);
function step(now) {
var t = Math.min((now - t0) / MORPH_MS, 1);
var e = easeOut(t);
var pts = start.map(function (p, i) {
var x = p[0] + (target[i][0] - p[0]) * e;
var y = p[1] + (target[i][1] - p[1]) * e;
return x.toFixed(1) + ',' + y.toFixed(1);
});
poly.setAttribute('points', pts.join(' '));
if (t < 1) { raf = requestAnimationFrame(step); }
else { current = target.map(function (p) { return p.slice(); }); }
}
raf = requestAnimationFrame(step);
}
var lastKey = 'N';
function paint(value) {
var lead = value.charAt(0);
var key = PREFIX_MAP[lead] || 'N';
if (key !== lastKey) {
morphTo(key);
lastKey = key;
}
tag.textContent = key === 'N' ? '패턴 미인식' : ('패턴 ' + key);
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
input.value = input.value.replace(/\D/g, '').slice(0, 16);
paint(input.value);
});
// 자동 루프: 앞자리 값을 바꿔가며 세 패턴을 순서대로 보여준다.
var SEQ = ['', '9999999999999995', '5399148803436467', '3739148803436', '', '9999999999999995'];
var si = 0;
var autoTimer = null;
function autoTick() {
input.value = SEQ[si];
paint(SEQ[si]);
si = (si + 1) % SEQ.length;
}
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } if (raf) { cancelAnimationFrame(raf); raf = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(autoTick, 1100); autoTick(); }
else { input.value = SEQ[1]; paint(SEQ[1]); }
})();import { useEffect, useRef } from "react";
import styles from "./NetworkIconMorph.module.scss";
type Box<T> = { current: T };
const MORPH_MS = 420; // 계산 겹: 앞자리 패턴 값이 바뀔 때 도형 점을 이만큼 동안 보간한다
// 가상 도형 3종(A/B/C) — 실제 카드사 로고·BIN 범위와 무관한 중립 라벨.
const SHAPES: Record<string, number[][]> = {
A: [[20, 4], [33.9, 12], [33.9, 28], [20, 36], [6.1, 28], [6.1, 12]],
B: [[29, 4.4], [33, 20], [29, 35.6], [13.5, 31.3], [2, 20], [13.5, 8.7]],
C: [[20, 1], [26.9, 16], [36.5, 29.5], [20, 28], [3.5, 29.5], [13.1, 16]],
N: [[20, 10], [28, 15], [28, 25], [20, 30], [12, 25], [12, 15]],
};
const PREFIX_MAP: Record<string, string> = { "3": "C", "4": "A", "5": "B" };
export default function NetworkIconMorph() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const input = root.querySelector(`.${styles.input}`) as HTMLInputElement | null;
const poly = root.querySelector(`.${styles.poly}`) as SVGPolygonElement | null;
const tag = root.querySelector(`.${styles.tag}`) as HTMLElement | null;
if (!input || !poly || !tag) return;
let current = SHAPES.N.map((p) => p.slice());
let raf: number | null = null;
function easeOut(t: number) { return 1 - Math.pow(1 - t, 3); }
function morphTo(key: string) {
const target = SHAPES[key];
const start = current.map((p) => p.slice());
const t0 = performance.now();
if (raf !== null) cancelAnimationFrame(raf);
function step(now: number) {
const t = Math.min((now - t0) / MORPH_MS, 1);
const e = easeOut(t);
const pts = start.map((p, i) => {
const x = p[0] + (target[i][0] - p[0]) * e;
const y = p[1] + (target[i][1] - p[1]) * e;
return x.toFixed(1) + "," + y.toFixed(1);
});
poly!.setAttribute("points", pts.join(" "));
if (t < 1) { raf = requestAnimationFrame(step); }
else { current = target.map((p) => p.slice()); }
}
raf = requestAnimationFrame(step);
}
let lastKey = "N";
function paint(value: string) {
const lead = value.charAt(0);
const key = PREFIX_MAP[lead] || "N";
if (key !== lastKey) {
morphTo(key);
lastKey = key;
}
tag!.textContent = key === "N" ? "패턴 미인식" : "패턴 " + key;
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
input!.value = input!.value.replace(/\D/g, "").slice(0, 16);
paint(input!.value);
}
input.addEventListener("input", onInput);
// 자동 루프: 앞자리 값을 바꿔가며 세 패턴을 순서대로 보여준다.
const SEQ = ["", "9999999999999995", "5399148803436467", "3739148803436", "", "9999999999999995"];
let si = 0;
let autoTimer: number | null = null;
function autoTick() {
input!.value = SEQ[si];
paint(SEQ[si]);
si = (si + 1) % SEQ.length;
}
function stopAutoplay() {
if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; }
if (raf !== null) { cancelAnimationFrame(raf); raf = null; }
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 1100); autoTick(); }
else { input.value = SEQ[1]; paint(SEQ[1]); }
return () => {
stopAutoplay();
input.removeEventListener("input", onInput);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.cn} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" inputMode="numeric" autoComplete="off" spellCheck={false} maxLength={16} defaultValue="" />
</label>
<div className={styles.shapeBox}>
<svg viewBox="0 0 40 40" width="34" height="34" aria-hidden="true">
<polygon className={styles.poly} points="20,10 28,15 28,25 20,30 12,25 12,15" />
</svg>
<span className={styles.tag}>패턴 미인식</span>
</div>
</div>
);
}08 패턴 인식 값 전환 — 앞자리 숫자 값이 정해진 패턴 값과 맞으면 옆의 도형 값이 다른 모양으로 바뀌는 방식 — 측정 누적면적 1.34% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식
이 데모가 속한 글
카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식카드 번호 입력 ui는 자릿수 값이 채워지는 상태에 따라 화면 여러 곳의 값이 함께 바뀌는 인터랙션인데, 아홉 개 중 하나는 입력된 숫자 값이 네 자리에 이를 때마다 그 사이 간격 값이 자동으로 생기고, 하나는 자릿 이 글의 데모 전부 보기
이 효과가 마음에 들면 — 비슷한 데모
입력 UI04 흔한 값 감점 — 입력 값이 미리 정해둔 흔한 값 목록과 같은 상태가 되면 강도 값이 깎이는 방식비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식
입력 UI02 부분선택 대시 전환 — 하위 항목이 일부만 선택되면 상위 박스의 표시 값이 대시 모양으로 바뀌는 방식커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식
입력 UI08 단위 플립 전환 — 단위가 바뀐 상태가 되면 카드가 뒤집히며 90도 지점에서 표시 값이 바뀌는 스텝퍼숫자 스텝퍼 UI 패턴 9종 — 길게 누를수록 빨라지는 것부터 눈금에 붙는 것까지