06인스타그램 포스트 임베드 카드 — 각도를 계속 다시 구해 흐르는 로딩 그라디언트
이미지가 들어갈 자리의 그라디언트 각도를 시간에 따라 계속 다시 계산해, 로딩이 끝나지 않은 것처럼 부드럽게 흘러가는 인스타그램 포스트 목업 카드입니다.
쓰임브랜드 갤러리 섹션, 후기 모음 페이지
linear-gradientsetPropertyborder-radius

<main class="stage">
<div class="card">
<div class="topBar">
<canvas class="avatar" id="avatar" width="64" height="64"></canvas>
<span class="handle">channel.a</span>
<span class="follow">팔로우</span>
</div>
<div class="slot" id="slot">
<div class="shimmer" id="shimmer" aria-hidden="true"></div>
</div>
<div class="bottomRow">
<svg class="heartIcon" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor">
<path d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54" opacity=".2"/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8"/>
</g></svg>
<span class="likeLabel">좋아요 128</span>
<span class="caption">새 게시물 미리보기</span>
</div>
</div>
<div class="readout" id="readout">각도 214°</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 사각 카드 임베드 — 그라디언트 회전각(계산 겹)과 shimmer 이동이 JS 계산 겹의 재료.
// CSS keyframes 는 두지 않는다 — 전부 rAF 실시간 루프.
@include stage($stage-yellow);
.stage {
overflow: hidden;
}
.card {
display: flex;
flex-direction: column;
width: 220px;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-raised;
overflow: hidden;
}
.topBar {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2 $sp-3;
}
.avatar {
display: block;
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
}
.handle {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 1.3;
color: $subject-ink;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
}
.follow {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: $subject-blue-deep;
flex-shrink: 0;
}
.slot {
position: relative;
height: 120px;
overflow: hidden;
--spin: 0deg;
background: linear-gradient(var(--spin), $subject-pink, $subject-sky, $subject-lilac);
}
.shimmer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 40%;
background: linear-gradient(
90deg,
rgba(255, 247, 230, 0) 0%,
rgba(255, 247, 230, .38) 50%,
rgba(255, 247, 230, 0) 100%
);
pointer-events: none;
}
.bottomRow {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2 $sp-3;
}
.heartIcon {
width: 18px;
height: 18px;
display: block;
flex-shrink: 0;
}
.heartIcon path {
fill: $subject-pink;
}
.likeLabel {
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: $subject-ink;
flex-shrink: 0;
}
.caption {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
color: $subject-ink;
opacity: .72;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
min-width: 0;
}
.readout {
margin-top: $sp-3;
display: inline-flex;
align-items: center;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $subject-cream;
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: $subject-ink;
}
@media (max-width: 340px) {
.card {
width: 156px;
}
.slot {
height: 64px;
}
.topBar {
padding: $sp-1 $sp-2;
}
.avatar {
width: 22px;
height: 22px;
}
.bottomRow {
padding: $sp-1 $sp-2;
}
.handle {
font-size: 11px;
}
.caption {
font-size: 10px;
}
.readout {
font-size: 10px;
padding: $sp-1 $sp-2;
margin-top: $sp-2;
}
}
@media (prefers-reduced-motion: reduce) {
.shimmer {
opacity: 0;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
overflow: hidden;
}
.card {
display: flex;
flex-direction: column;
width: 220px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.topBar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.avatar {
display: block;
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
}
.handle {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 1.3;
color: #17141a;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
}
.follow {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: #0a4fe8;
flex-shrink: 0;
}
.slot {
position: relative;
height: 120px;
overflow: hidden;
--spin: 0deg;
background: linear-gradient(var(--spin), #ff9ebb, #6ec8ff, #b9a0ff);
}
.shimmer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 40%;
background: linear-gradient(90deg, rgba(255, 247, 230, 0) 0%, rgba(255, 247, 230, 0.38) 50%, rgba(255, 247, 230, 0) 100%);
pointer-events: none;
}
.bottomRow {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.heartIcon {
width: 18px;
height: 18px;
display: block;
flex-shrink: 0;
}
.heartIcon path {
fill: #ff9ebb;
}
.likeLabel {
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: #17141a;
flex-shrink: 0;
}
.caption {
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
color: #17141a;
opacity: 0.72;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
min-width: 0;
}
.readout {
margin-top: 12px;
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 999px;
background: #fff7e6;
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: #17141a;
}
@media (max-width: 340px) {
.card {
width: 156px;
}
.slot {
height: 64px;
}
.topBar {
padding: 4px 8px;
}
.avatar {
width: 22px;
height: 22px;
}
.bottomRow {
padding: 4px 8px;
}
.handle {
font-size: 11px;
}
.caption {
font-size: 10px;
}
.readout {
font-size: 10px;
padding: 4px 8px;
margin-top: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.shimmer {
opacity: 0;
}
}// 사각 카드 임베드(06) — 이미지 자리는 각도가 도는 그라디언트(계산 겹)로 채우고,
// 그 위를 옅은 띠(shimmer)가 가로질러 로딩 느낌을 더한다. 실키·실서버 요청 없음.
(function () {
function mulberry32(seed) {
var a = seed >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function drawSeedThumb(canvas, seed) {
var ctx = canvas.getContext('2d');
if (!ctx) return;
var rnd = mulberry32(seed);
var w = canvas.width, h = canvas.height;
var hues = [216, 32, 268, 168];
ctx.fillStyle = 'hsl(' + hues[Math.floor(rnd() * hues.length)] + ', 38%, 22%)';
ctx.fillRect(0, 0, w, h);
for (var i = 0; i < 6; i++) {
var cx = rnd() * w, cy = rnd() * h, r = (0.18 + rnd() * 0.3) * Math.min(w, h);
ctx.fillStyle = 'hsla(' + hues[Math.floor(rnd() * hues.length)] + ', ' + (44 + rnd() * 22).toFixed(1) + '%, ' + (38 + rnd() * 26).toFixed(1) + '%, .5)';
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
}
ctx.fillStyle = 'rgba(255, 247, 230, .06)';
for (var j = 0; j < h; j += 4) ctx.fillRect(0, j, w, 1);
}
var avatarEl = document.getElementById('avatar');
var slotEl = document.getElementById('slot');
var shimmerEl = document.getElementById('shimmer');
var readoutEl = document.getElementById('readout');
if (!avatarEl || !slotEl || !shimmerEl || !readoutEl) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
drawSeedThumb(avatarEl, 106);
var CYCLE_MS = 2000;
function apply(t) {
// 계산 겹: 경과 시간 비율 → 그라디언트 회전각(0~360도)
var angle = Math.floor((t / CYCLE_MS) * 360);
slotEl.style.setProperty('--spin', angle + 'deg');
var prog = t / CYCLE_MS;
var sweep = -30 + prog * 160;
shimmerEl.style.transform = 'translateX(' + sweep.toFixed(1) + '%)';
readoutEl.textContent = '각도 ' + angle + '°';
}
if (reduce) {
apply(0);
return;
}
var start = null;
function loop(ts) {
if (start === null) start = ts;
var t = (ts - start) % CYCLE_MS;
apply(t);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();import { useEffect, useRef } from "react";
import styles from "./EmbedInstagram.module.scss";
// 사각 카드 임베드 — 이미지 자리는 각도가 도는 그라디언트(계산 겹)로 채우고,
// 그 위를 옅은 띠(shimmer)가 가로질러 로딩 느낌을 더한다. 실키·실서버 요청 없음.
const CYCLE_MS = 2000;
function mulberry32(seed: number) {
let a = seed >>> 0;
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function drawSeedThumb(canvas: HTMLCanvasElement, seed: number) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const rnd = mulberry32(seed);
const w = canvas.width;
const h = canvas.height;
const hues = [216, 32, 268, 168];
ctx.fillStyle = "hsl(" + hues[Math.floor(rnd() * hues.length)] + ", 38%, 22%)";
ctx.fillRect(0, 0, w, h);
for (let i = 0; i < 6; i++) {
const cx = rnd() * w;
const cy = rnd() * h;
const r = (0.18 + rnd() * 0.3) * Math.min(w, h);
ctx.fillStyle =
"hsla(" + hues[Math.floor(rnd() * hues.length)] + ", " + (44 + rnd() * 22).toFixed(1) + "%, " + (38 + rnd() * 26).toFixed(1) + "%, .5)";
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "rgba(255, 247, 230, .06)";
for (let j = 0; j < h; j += 4) ctx.fillRect(0, j, w, 1);
}
export default function EmbedInstagram() {
const avatarRef = useRef<HTMLCanvasElement>(null);
const slotRef = useRef<HTMLDivElement>(null);
const shimmerRef = useRef<HTMLDivElement>(null);
const readoutRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const avatarEl = avatarRef.current;
const slotEl = slotRef.current;
const shimmerEl = shimmerRef.current;
const readoutEl = readoutRef.current;
if (!avatarEl || !slotEl || !shimmerEl || !readoutEl) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
drawSeedThumb(avatarEl, 106);
function apply(t: number) {
// 계산 겹: 경과 시간 비율 → 그라디언트 회전각(0~360도)
const angle = Math.floor((t / CYCLE_MS) * 360);
slotEl!.style.setProperty("--spin", angle + "deg");
const prog = t / CYCLE_MS;
const sweep = -30 + prog * 160;
shimmerEl!.style.transform = "translateX(" + sweep.toFixed(1) + "%)";
readoutEl!.textContent = "각도 " + angle + "°";
}
if (reduce) {
apply(0);
return;
}
let start: number | null = null;
let raf = 0;
function loop(ts: number) {
if (start === null) start = ts;
const t = (ts - start) % CYCLE_MS;
apply(t);
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
return (
<div className={styles.stage}>
<div className={styles.card}>
<div className={styles.topBar}>
<canvas className={styles.avatar} ref={avatarRef} width={64} height={64} />
<span className={styles.handle}>channel.a</span>
<span className={styles.follow}>팔로우</span>
</div>
<div className={styles.slot} ref={slotRef}>
<div className={styles.shimmer} ref={shimmerRef} aria-hidden="true" />
</div>
<div className={styles.bottomRow}>
<svg className={styles.heartIcon} viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<path
d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54"
opacity=".2"
/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8" />
</g>
</svg>
<span className={styles.likeLabel}>좋아요 128</span>
<span className={styles.caption}>새 게시물 미리보기</span>
</div>
</div>
<div className={styles.readout} ref={readoutRef}>
각도 214°
</div>
</div>
);
}06 인스타그램 포스트 임베드 카드 — 각도를 계속 다시 구해 흐르는 로딩 그라디언트 — 측정 누적면적 18.658% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 유튜브 임베드 코드 9종 — SNS 카드도 담은 플레이스홀더 UI
이 데모가 속한 글
유튜브 임베드 코드 9종 — SNS 카드도 담은 플레이스홀더 UI프레임 폭에 맞춰 세로 비율이 다시 계산되고, 화면에 머무른 시간이 일정 값을 넘으면 가벼운 카드가 스스로 완전한 프레임으로 바뀌고, 최근 소셜 임베드는 등장 뒤 지난 시간만큼 배지 숫자가 조금씩 올라가는 임베드 U 이 글의 데모 전부 보기


