09여러 방식을 한 폼에 모은 회원가입 히어로 장면
01 아이콘 모프, 05 자동입력 글로우, 06 생성 캐스케이드를 한 회원가입 폼 위에 동시에 배치해 완성 장면을 만든다.
쓰임랜딩 페이지 회원가입 섹션
HERO_CASCADE_STEP_MSheroSimulateAutofillheroMorphEye

<main class="stage">
<div class="phs is-demo" id="phsRoot">
<form class="phs__card" id="phsForm">
<p class="phs__title">회원가입</p>
<div class="phs__row">
<label class="phs__label" for="phsEmail">이메일</label>
<div class="phs__staticField" id="phsEmail">[email protected]</div>
</div>
<div class="phs__row">
<label class="phs__label" for="phsPw">비밀번호</label>
<div class="phs__field" id="phsField">
<input class="phs__input" id="phsPw" type="password" aria-describedby="phsHint">
<button class="phs__eyeBtn" id="phsEyeBtn" type="button" aria-pressed="false" aria-label="비밀번호 보기">
<svg class="phs__eye" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path class="phs__lidTop" id="phsLidTop" d="M2,12 Q12,4 22,12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path class="phs__lidBottom" id="phsLidBottom" d="M2,12 Q12,20 22,12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle class="phs__pupil" id="phsPupil" cx="12" cy="12" r="3.2" fill="currentColor"/>
</svg>
</button>
</div>
</div>
<div class="phs__actions">
<button class="phs__autofillBtn" id="phsAutofillBtn" type="button">자동입력 흉내</button>
<button class="phs__generateBtn" id="phsGenerateBtn" type="button">
<span>생성</span>
<span class="phs__icoWrap" aria-hidden="true">
<svg class="phs__copyIcon" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg>
<span class="phs__checkIcon">
<span class="phs__checkBar phs__checkBar--short"></span>
<span class="phs__checkBar phs__checkBar--long"></span>
</span>
</span>
</button>
</div>
<p class="phs__hint" id="phsHint">자동입력 흉내와 생성 버튼은 실제 클릭이 부르는 함수를 그대로 다시 부른다</p>
</form>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-ink);
.phs {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-ink;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: $sp-5;
}
.phs__card {
width: 100%;
max-width: 21em;
display: flex;
flex-direction: column;
gap: $sp-3;
margin: 0;
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.phs__title {
margin: 0;
font-weight: 700;
font-size: 15px;
letter-spacing: .02em;
}
.phs__row {
display: flex;
flex-direction: column;
gap: $sp-1;
}
.phs__label {
font-weight: 600;
font-size: 11px;
color: rgba(255, 247, 230, .64);
}
.phs__staticField {
--glow: 0;
border-radius: $r-control;
padding: $sp-2 $sp-3;
background: rgba(255, 247, 230, .07);
border: 1px solid rgba(255, 247, 230, .16);
color: rgba(255, 247, 230, .8);
font-size: 13px;
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(.2 + var(--glow) * .6));
}
.phs__field {
--glow: 0;
display: flex;
align-items: center;
gap: $sp-2;
border-radius: $r-control;
padding: $sp-2 $sp-3;
background: rgba(255, 247, 230, .07);
border: 1px solid rgba(255, 247, 230, .16);
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(.2 + var(--glow) * .6));
}
.phs__input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: $subject-cream;
font-size: 13px;
letter-spacing: .06em;
font-family: inherit;
min-width: 0;
}
.phs__eyeBtn {
display: grid;
place-items: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: $r-pill;
background: transparent;
color: $subject-cream;
cursor: pointer;
flex: none;
}
.phs__eyeBtn:focus-visible {
outline: 2px solid $subject-cream;
outline-offset: 2px;
}
.phs__actions {
display: flex;
gap: $sp-2;
margin-top: $sp-1;
}
.phs__autofillBtn, .phs__generateBtn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $sp-1;
border: none;
border-radius: $r-control;
padding: $sp-2 $sp-3;
font-family: inherit;
font-weight: 600;
font-size: 12px;
cursor: pointer;
}
.phs__autofillBtn {
background: rgba(255, 247, 230, .1);
color: $subject-cream;
}
.phs__generateBtn {
background: $subject-cream;
color: $subject-ink;
}
.phs__autofillBtn:focus-visible, .phs__generateBtn:focus-visible {
outline: 2px solid $subject-cream;
outline-offset: 2px;
}
.phs__icoWrap {
position: relative;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
}
.phs__copyIcon {
width: 14px;
height: 14px;
opacity: 1;
transition: opacity $dur-quick $ease-out;
}
.phs__checkIcon {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity $dur-quick $ease-out;
}
.phs__checkBar {
position: absolute;
left: 1px;
top: 7px;
height: 2px;
background: currentColor;
border-radius: $r-xs;
transform-origin: left center;
}
.phs__checkBar--short { width: 5px; transform: rotate(225deg); }
.phs__checkBar--long { width: 10px; transform: rotate(-48deg); }
.phs.is-generated .phs__copyIcon { opacity: 0; }
.phs.is-generated .phs__checkIcon { opacity: 1; }
// 배경 톤: 필드 안쪽 골드 glow 와 같은 톤을 is-generated 순간 카드 전체로 넓힌다(새 색 없음).
.phs::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 210, 63, 0);
transition: background-color .3s ease;
}
.phs.is-generated::before {
background: rgba(255, 210, 63, .1);
}
.phs__hint {
margin: $sp-1 0 0;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, .56);
}
@media (max-width: 340px) {
.phs { padding: $sp-3; }
.phs__card { gap: $sp-2; }
.phs__hint { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.phs__copyIcon, .phs__checkIcon { transition: none; }
.phs::before { 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);
}
.phs {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #17141a;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 20px;
}
.phs__card {
width: 100%;
max-width: 21em;
display: flex;
flex-direction: column;
gap: 12px;
margin: 0;
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.phs__title {
margin: 0;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.phs__row {
display: flex;
flex-direction: column;
gap: 4px;
}
.phs__label {
font-weight: 600;
font-size: 11px;
color: rgba(255, 247, 230, 0.64);
}
.phs__staticField {
--glow: 0;
border-radius: 12px;
padding: 8px 12px;
background: rgba(255, 247, 230, 0.07);
border: 1px solid rgba(255, 247, 230, 0.16);
color: rgba(255, 247, 230, 0.8);
font-size: 13px;
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(0.2 + var(--glow) * 0.6));
}
.phs__field {
--glow: 0;
display: flex;
align-items: center;
gap: 8px;
border-radius: 12px;
padding: 8px 12px;
background: rgba(255, 247, 230, 0.07);
border: 1px solid rgba(255, 247, 230, 0.16);
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(0.2 + var(--glow) * 0.6));
}
.phs__input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #fff7e6;
font-size: 13px;
letter-spacing: 0.06em;
font-family: inherit;
min-width: 0;
}
.phs__eyeBtn {
display: grid;
place-items: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: #fff7e6;
cursor: pointer;
flex: none;
}
.phs__eyeBtn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.phs__actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.phs__autofillBtn, .phs__generateBtn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
border: none;
border-radius: 12px;
padding: 8px 12px;
font-family: inherit;
font-weight: 600;
font-size: 12px;
cursor: pointer;
}
.phs__autofillBtn {
background: rgba(255, 247, 230, 0.1);
color: #fff7e6;
}
.phs__generateBtn {
background: #fff7e6;
color: #17141a;
}
.phs__autofillBtn:focus-visible, .phs__generateBtn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.phs__icoWrap {
position: relative;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
}
.phs__copyIcon {
width: 14px;
height: 14px;
opacity: 1;
transition: opacity 220ms ease-out;
}
.phs__checkIcon {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 220ms ease-out;
}
.phs__checkBar {
position: absolute;
left: 1px;
top: 7px;
height: 2px;
background: currentColor;
border-radius: 4px;
transform-origin: left center;
}
.phs__checkBar--short {
width: 5px;
transform: rotate(225deg);
}
.phs__checkBar--long {
width: 10px;
transform: rotate(-48deg);
}
.phs.is-generated .phs__copyIcon {
opacity: 0;
}
.phs.is-generated .phs__checkIcon {
opacity: 1;
}
.phs::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 210, 63, 0);
transition: background-color 0.3s ease;
}
.phs.is-generated::before {
background: rgba(255, 210, 63, 0.1);
}
.phs__hint {
margin: 4px 0 0;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, 0.56);
}
@media (max-width: 340px) {
.phs {
padding: 12px;
}
.phs__card {
gap: 8px;
}
.phs__hint {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.phs__copyIcon, .phs__checkIcon {
transition: none;
}
.phs::before {
transition: none;
}
}(function () {
// 기법: 눈 모프·글로우 펄스·생성 캐스케이드 세 계산을 이 파일 안에서 독립적으로 다시 구현한다
// (01/05/06 과 이름이 겹치지 않도록 hero* 접두 함수·HERO_* 상수로 새로 짓는다). 눈은 progress
// 를 목표까지 rAF 로 보간해 베지어 ctrlY 를 다시 계산하고, 글로우는 sin(경과비*PI)*exp(-경과비*k)
// 엔벌로프로 자동입력 하이라이트를 표현하며, 생성은 누적 타이머로 글자를 한 자씩 채운 뒤
// 복사 아이콘을 체크로 크로스페이드한다.
var root = document.getElementById('phsRoot');
var form = document.getElementById('phsForm');
var emailField = document.getElementById('phsEmail');
var pwField = document.getElementById('phsField');
var pwInput = document.getElementById('phsPw');
var eyeBtn = document.getElementById('phsEyeBtn');
var lidTop = document.getElementById('phsLidTop');
var lidBottom = document.getElementById('phsLidBottom');
var pupil = document.getElementById('phsPupil');
var autofillBtn = document.getElementById('phsAutofillBtn');
var generateBtn = document.getElementById('phsGenerateBtn');
var HERO_EYE_MORPH_MS = 260;
var HERO_GLOW_PULSE_MS = 900;
var HERO_CASCADE_STEP_MS = 70;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var pwRand = seedRand(426091);
var cascadeRand = seedRand(426092);
var PW_POOL = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
function pickPassword(len, rand) {
var out = '';
for (var i = 0; i < len; i += 1) out += PW_POOL[Math.floor(rand() * PW_POOL.length)];
return out;
}
pwInput.value = pickPassword(9, pwRand);
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
function heroMorphEye(progress) {
var eased = easeOutCubic(progress);
var lift = 8 * eased;
lidTop.setAttribute('d', 'M2,12 Q12,' + (12 - lift) + ' 22,12');
lidBottom.setAttribute('d', 'M2,12 Q12,' + (12 + lift) + ' 22,12');
pupil.style.opacity = String(eased);
}
heroMorphEye(0);
var eyeProgress = 0, eyeFrom = 0, eyeTarget = 0, eyeStart = 0, eyeRafId = null;
function eyeStep(ts) {
var t = Math.min(1, (ts - eyeStart) / HERO_EYE_MORPH_MS);
eyeProgress = eyeFrom + (eyeTarget - eyeFrom) * easeOutCubic(t);
heroMorphEye(eyeProgress);
if (t < 1) { eyeRafId = requestAnimationFrame(eyeStep); } else { eyeRafId = null; }
}
function animateEyeTo(target) {
eyeFrom = eyeProgress;
eyeTarget = target;
eyeStart = performance.now();
if (eyeRafId) cancelAnimationFrame(eyeRafId);
eyeRafId = requestAnimationFrame(eyeStep);
}
function toggleEye() {
var willShow = pwInput.type === 'password';
pwInput.type = willShow ? 'text' : 'password';
eyeBtn.setAttribute('aria-pressed', String(willShow));
animateEyeTo(willShow ? 1 : 0);
}
var glowStart = 0, glowRafId = null;
function setGlow(v) {
emailField.style.setProperty('--glow', String(v));
pwField.style.setProperty('--glow', String(v));
}
function glowStep(ts) {
var frac = (ts - glowStart) / HERO_GLOW_PULSE_MS;
if (frac >= 1) { setGlow(0); glowRafId = null; return; }
var envelope = Math.max(0, Math.sin(frac * Math.PI) * Math.exp(-frac * 2.2));
setGlow(envelope);
glowRafId = requestAnimationFrame(glowStep);
}
function heroSimulateAutofill() {
glowStart = performance.now();
if (glowRafId) return;
glowRafId = requestAnimationFrame(glowStep);
}
var cascadeTimer = null;
function heroRunCascade() {
var target = pickPassword(9, cascadeRand);
var i = 0;
pwInput.type = 'text';
eyeBtn.setAttribute('aria-pressed', 'true');
animateEyeTo(1);
pwInput.value = '';
root.classList.remove('is-generated');
window.clearTimeout(cascadeTimer);
function step() {
i += 1;
pwInput.value = target.slice(0, i);
if (i < target.length) {
cascadeTimer = window.setTimeout(step, HERO_CASCADE_STEP_MS);
} else {
root.classList.add('is-generated');
cascadeTimer = window.setTimeout(function () {
root.classList.remove('is-generated');
}, 500);
}
}
step();
}
eyeBtn.addEventListener('click', function () { toggleEye(); });
autofillBtn.addEventListener('click', function () { heroSimulateAutofill(); });
generateBtn.addEventListener('click', function () { heroRunCascade(); });
form.addEventListener('submit', function (e) { e.preventDefault(); });
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) root.classList.remove('is-demo');
var demoTimers = [];
function clearDemoTimers() {
for (var i = 0; i < demoTimers.length; i += 1) window.clearTimeout(demoTimers[i]);
demoTimers = [];
}
function demoLoop() {
if (!root.classList.contains('is-demo')) return;
clearDemoTimers();
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 150));
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 650));
demoTimers.push(window.setTimeout(function () { heroSimulateAutofill(); }, 900));
demoTimers.push(window.setTimeout(function () { heroRunCascade(); }, 1500));
demoTimers.push(window.setTimeout(function () {
pwInput.type = 'password';
eyeBtn.setAttribute('aria-pressed', 'false');
animateEyeTo(0);
}, 2600));
demoTimers.push(window.setTimeout(demoLoop, 2800));
}
if (!reduce) demoTimers.push(window.setTimeout(demoLoop, 200));
})();import { useEffect, useRef } from "react";
import styles from "./PasswordRevealHeroSignupFormScene.module.scss";
export default function PasswordRevealHeroSignupFormScene() {
const rootRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const emailFieldRef = useRef<HTMLDivElement>(null);
const pwFieldRef = useRef<HTMLDivElement>(null);
const pwInputRef = useRef<HTMLInputElement>(null);
const eyeBtnRef = useRef<HTMLButtonElement>(null);
const lidTopRef = useRef<SVGPathElement>(null);
const lidBottomRef = useRef<SVGPathElement>(null);
const pupilRef = useRef<SVGCircleElement>(null);
const autofillBtnRef = useRef<HTMLButtonElement>(null);
const generateBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = rootRef.current;
const form = formRef.current;
const emailField = emailFieldRef.current;
const pwField = pwFieldRef.current;
const pwInput = pwInputRef.current;
const eyeBtn = eyeBtnRef.current;
const lidTop = lidTopRef.current;
const lidBottom = lidBottomRef.current;
const pupil = pupilRef.current;
const autofillBtn = autofillBtnRef.current;
const generateBtn = generateBtnRef.current;
if (!root || !form || !emailField || !pwField || !pwInput || !eyeBtn || !lidTop || !lidBottom || !pupil || !autofillBtn || !generateBtn) return;
const HERO_EYE_MORPH_MS = 260;
const HERO_GLOW_PULSE_MS = 900;
const HERO_CASCADE_STEP_MS = 70;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const pwRand = seedRand(426093);
const cascadeRand = seedRand(426094);
const PW_POOL = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%";
function pickPassword(len: number, rand: () => number) {
let out = "";
for (let i = 0; i < len; i += 1) out += PW_POOL[Math.floor(rand() * PW_POOL.length)];
return out;
}
pwInput.value = pickPassword(9, pwRand);
function easeOutCubic(t: number) { return 1 - Math.pow(1 - t, 3); }
function heroMorphEye(progress: number) {
const eased = easeOutCubic(progress);
const lift = 8 * eased;
lidTop!.setAttribute("d", "M2,12 Q12," + (12 - lift) + " 22,12");
lidBottom!.setAttribute("d", "M2,12 Q12," + (12 + lift) + " 22,12");
pupil!.style.opacity = String(eased);
}
heroMorphEye(0);
let eyeProgress = 0, eyeFrom = 0, eyeTarget = 0, eyeStart = 0, eyeRafId = 0;
function eyeStep(ts: number) {
const t = Math.min(1, (ts - eyeStart) / HERO_EYE_MORPH_MS);
eyeProgress = eyeFrom + (eyeTarget - eyeFrom) * easeOutCubic(t);
heroMorphEye(eyeProgress);
if (t < 1) { eyeRafId = requestAnimationFrame(eyeStep); }
}
function animateEyeTo(target: number) {
eyeFrom = eyeProgress;
eyeTarget = target;
eyeStart = performance.now();
cancelAnimationFrame(eyeRafId);
eyeRafId = requestAnimationFrame(eyeStep);
}
function toggleEye() {
const willShow = pwInput!.type === "password";
pwInput!.type = willShow ? "text" : "password";
eyeBtn!.setAttribute("aria-pressed", String(willShow));
animateEyeTo(willShow ? 1 : 0);
}
let glowStart = 0, glowRafId = 0, glowing = false;
function setGlow(v: number) {
emailField!.style.setProperty("--glow", String(v));
pwField!.style.setProperty("--glow", String(v));
}
function glowStep(ts: number) {
const frac = (ts - glowStart) / HERO_GLOW_PULSE_MS;
if (frac >= 1) { setGlow(0); glowing = false; return; }
const envelope = Math.max(0, Math.sin(frac * Math.PI) * Math.exp(-frac * 2.2));
setGlow(envelope);
glowRafId = requestAnimationFrame(glowStep);
}
function heroSimulateAutofill() {
glowStart = performance.now();
if (glowing) return;
glowing = true;
glowRafId = requestAnimationFrame(glowStep);
}
let cascadeTimer = 0;
function heroRunCascade() {
const target = pickPassword(9, cascadeRand);
let i = 0;
pwInput!.type = "text";
eyeBtn!.setAttribute("aria-pressed", "true");
animateEyeTo(1);
pwInput!.value = "";
root!.classList.remove("is-generated");
window.clearTimeout(cascadeTimer);
function step() {
i += 1;
pwInput!.value = target.slice(0, i);
if (i < target.length) {
cascadeTimer = window.setTimeout(step, HERO_CASCADE_STEP_MS);
} else {
root!.classList.add("is-generated");
cascadeTimer = window.setTimeout(function () {
root!.classList.remove("is-generated");
}, 500);
}
}
step();
}
function onEyeClick() { toggleEye(); }
function onAutofillClick() { heroSimulateAutofill(); }
function onGenerateClick() { heroRunCascade(); }
function onSubmit(e: Event) { e.preventDefault(); }
eyeBtn.addEventListener("click", onEyeClick);
autofillBtn.addEventListener("click", onAutofillClick);
generateBtn.addEventListener("click", onGenerateClick);
form.addEventListener("submit", onSubmit);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) root.classList.remove("is-demo");
let demoTimers: number[] = [];
function clearDemoTimers() {
for (let i = 0; i < demoTimers.length; i += 1) window.clearTimeout(demoTimers[i]);
demoTimers = [];
}
function demoLoop() {
if (!root!.classList.contains("is-demo")) return;
clearDemoTimers();
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 150));
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 650));
demoTimers.push(window.setTimeout(function () { heroSimulateAutofill(); }, 900));
demoTimers.push(window.setTimeout(function () { heroRunCascade(); }, 1500));
demoTimers.push(window.setTimeout(function () {
pwInput!.type = "password";
eyeBtn!.setAttribute("aria-pressed", "false");
animateEyeTo(0);
}, 2600));
demoTimers.push(window.setTimeout(demoLoop, 2800));
}
if (!reduce) demoTimers.push(window.setTimeout(demoLoop, 200));
return () => {
eyeBtn.removeEventListener("click", onEyeClick);
autofillBtn.removeEventListener("click", onAutofillClick);
generateBtn.removeEventListener("click", onGenerateClick);
form.removeEventListener("submit", onSubmit);
cancelAnimationFrame(eyeRafId);
cancelAnimationFrame(glowRafId);
window.clearTimeout(cascadeTimer);
clearDemoTimers();
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.phs} is-demo`} ref={rootRef}>
<form className={styles.phs__card} ref={formRef}>
<p className={styles.phs__title}>회원가입</p>
<div className={styles.phs__row}>
<label className={styles.phs__label} htmlFor="phsEmailReact">이메일</label>
<div className={styles.phs__staticField} id="phsEmailReact" ref={emailFieldRef}>[email protected]</div>
</div>
<div className={styles.phs__row}>
<label className={styles.phs__label} htmlFor="phsPwReact">비밀번호</label>
<div className={styles.phs__field} ref={pwFieldRef}>
<input className={styles.phs__input} id="phsPwReact" type="password" aria-describedby="phsHintReact" ref={pwInputRef} readOnly />
<button className={styles.phs__eyeBtn} type="button" aria-pressed="false" aria-label="비밀번호 보기" ref={eyeBtnRef}>
<svg className={styles.phs__eye} viewBox="0 0 24 24" width={18} height={18} aria-hidden="true">
<path className={styles.phs__lidTop} ref={lidTopRef} d="M2,12 Q12,4 22,12" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
<path className={styles.phs__lidBottom} ref={lidBottomRef} d="M2,12 Q12,20 22,12" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
<circle className={styles.phs__pupil} ref={pupilRef} cx={12} cy={12} r={3.2} fill="currentColor" />
</svg>
</button>
</div>
</div>
<div className={styles.phs__actions}>
<button className={styles.phs__autofillBtn} type="button" ref={autofillBtnRef}>자동입력 흉내</button>
<button className={styles.phs__generateBtn} type="button" ref={generateBtnRef}>
<span>생성</span>
<span className={styles.phs__icoWrap} aria-hidden="true">
<svg className={styles.phs__copyIcon} width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M216 40v128h-48V88H88V40Z" opacity=".2" />
<path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
</g>
</svg>
<span className={styles.phs__checkIcon}>
<span className={`${styles.phs__checkBar} ${styles["phs__checkBar--short"]}`} />
<span className={`${styles.phs__checkBar} ${styles["phs__checkBar--long"]}`} />
</span>
</span>
</button>
</div>
<p className={styles.phs__hint} id="phsHintReact">자동입력 흉내와 생성 버튼은 실제 클릭이 부르는 함수를 그대로 다시 부른다</p>
</form>
</div>
</main>
);
}09 여러 방식을 한 폼에 모은 회원가입 히어로 장면 — 측정 누적면적 75.046% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 표시 아이콘 9가지 — 입력창 속 문자가 도트로 바뀌는 계산
이 데모가 속한 글
비밀번호 표시 아이콘 9가지 — 입력창 속 문자가 도트로 바뀌는 계산비밀번호 표시 아이콘 밑에는 아홉 갈래로 갈라지는 골목이 있다. 01번 골목은 글자와 점이 자리를 바꾸는 교차로이고, 05번 골목은 빛이 벽을 타고 번지는 통로이며, 06번 골목은 글자가 한 걸음씩 걸어 들어오는 계 이 글의 데모 전부 보기


