09Background made from a name
Folding the letters into a number and using it as the seed makes one name always return the same pattern.
Best forProfile header background, a per-person card backdrop in a team list
charCodeAtMath.imulanimation-delay
<main class="stage">
<div class="sa is-demo" id="root">
<svg class="sa__art" id="art" viewBox="0 0 240 144" role="img" data-label="A left-right symmetric background folded out of a name, seed {n}"></svg>
<div class="sa__bar">
<p class="sa__seed">
<svg xmlns="http://www.w3.org/2000/svg" class="sa__ic" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M216 64v128a24 24 0 0 1-24 24H64a24 24 0 0 1-24-24V64a24 24 0 0 1 24-24h128a24 24 0 0 1 24 24" opacity=".2"/><path d="M192 32H64a32 32 0 0 0-32 32v128a32 32 0 0 0 32 32h128a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32m16 160a16 16 0 0 1-16 16H64a16 16 0 0 1-16-16V64a16 16 0 0 1 16-16h128a16 16 0 0 1 16 16ZM104 92a12 12 0 1 1-12-12a12 12 0 0 1 12 12m72 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12m-72 72a12 12 0 1 1-12-12a12 12 0 0 1 12 12m36-36a12 12 0 1 1-12-12a12 12 0 0 1 12 12m36 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg>
Seed <b id="num">godrich</b>
</p>
<button class="sa__btn" type="button" id="again">
<svg xmlns="http://www.w3.org/2000/svg" class="sa__ic" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M107.14 98.8L128 128l-20.86 29.2A64 64 0 0 1 55.06 184H32V72h23.06a64 64 0 0 1 52.08 26.8m93.8-26.8a64 64 0 0 0-52.08 26.8L128 128l20.86 29.2a64 64 0 0 0 52.08 26.8H232V72Z" opacity=".2"/><path d="M237.66 178.34a8 8 0 0 1 0 11.32l-24 24a8 8 0 0 1-11.32-11.32L212.69 192h-11.75a72.12 72.12 0 0 1-58.59-30.15l-41.72-58.4A56.1 56.1 0 0 0 55.06 80H32a8 8 0 0 1 0-16h23.06a72.12 72.12 0 0 1 58.59 30.15l41.72 58.4A56.1 56.1 0 0 0 200.94 176h11.75l-10.35-10.34a8 8 0 0 1 11.32-11.32ZM143 107a8 8 0 0 0 11.16-1.86l1.2-1.67A56.1 56.1 0 0 1 200.94 80h11.75l-10.35 10.34a8 8 0 0 0 11.32 11.32l24-24a8 8 0 0 0 0-11.32l-24-24a8 8 0 0 0-11.32 11.32L212.69 64h-11.75a72.12 72.12 0 0 0-58.59 30.15l-1.2 1.67A8 8 0 0 0 143 107m-30 42a8 8 0 0 0-11.16 1.86l-1.2 1.67A56.1 56.1 0 0 1 55.06 176H32a8 8 0 0 0 0 16h23.06a72.12 72.12 0 0 0 58.59-30.15l1.2-1.67A8 8 0 0 0 113 149"/></g></svg>
<span class="sa__label">Regenerate</span>
</button>
<button class="sa__btn" type="button" id="copy" data-done="Copied">
<svg xmlns="http://www.w3.org/2000/svg" class="sa__ic" viewBox="0 0 256 256" aria-hidden="true"><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="sa__label">Copy SVG</span>
</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 이름으로 만든 배경 — 시드가 숫자가 아니라 글자열이다. FNV-1a 로 접어 얻은 32비트 정수를
// 그대로 난수의 시드로 쓰면 같은 이름은 언제나 같은 무늬로 돌아온다.
// 미리보기 루프는 대각선을 타는 물결이라 시작과 끝이 같은 완성 배경이다 — 조각이 사라지지 않는다.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-yellow);
.sa { display: grid; justify-items: center; gap: $sp-3; width: 100%; }
.sa__art { display: block; width: clamp(176px, 60vw, 320px); height: auto; }
// transform-box: fill-box 가 없으면 % 원점이 SVG 뷰포트 기준이라 조각이 제자리에서 안 커진다
.sa__bit {
transform-box: fill-box;
transform-origin: 50% 50%;
}
.sa__bit--a { fill: $color; }
.sa__bit--b { fill: $subject-blue; }
.sa__bit--c { fill: $subject-cream; }
.sa__bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: $sp-2; }
.sa__seed {
margin: 0;
display: inline-flex; align-items: center; gap: $sp-1;
color: rgba(23, 20, 26, .78);
font-weight: 700; font-size: clamp(9px, 2.4vw, 11px);
}
.sa__btn {
display: inline-flex; align-items: center; gap: $sp-1;
border: 0; border-radius: $r-pill;
padding: $sp-1 $sp-3;
background: $subject-ink; color: $subject-cream;
font-family: inherit; font-weight: 700; font-size: clamp(9px, 2.4vw, 11px);
cursor: pointer;
}
.sa__btn:focus-visible { outline: 3px solid $subject-blue; outline-offset: 2px; }
.sa__ic { width: 12px; height: 12px; flex: none; }
// ── 미리보기 루프: 좌우 대칭이라 물결도 양쪽에서 같이 온다
.sa.is-demo .sa__bit {
animation-name: saPulse;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * -124ms); // 음수 지연이라 첫 프레임부터 물결이 이미 퍼져 있다
}
// 사람이 「다시 생성」을 눌러 다음 이름으로 넘어가면 새 무늬가 대각선 순으로 찍힌다
.sa.is-draw .sa__bit {
animation-name: saStamp;
animation-duration: $dur-enter;
animation-timing-function: $ease-pop;
animation-iteration-count: 1;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 28ms);
}
@keyframes saPulse {
0% { transform: scale(1) rotate(0deg); }
28% { transform: scale(.5) rotate(-18deg); }
56% { transform: scale(1.08) rotate(6deg); }
80% { transform: scale(1) rotate(0deg); }
100% { transform: scale(1) rotate(0deg); }
}
@keyframes saStamp {
from { transform: scale(.2) rotate(-30deg); opacity: 0; }
to { transform: scale(1) rotate(0deg); opacity: 1; }
}
@media (max-width: 340px) {
.sa { gap: $sp-2; }
.sa__ic { width: 10px; height: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.sa.is-demo .sa__bit,
.sa.is-draw .sa__bit { animation: none !important; transform: scale(1) rotate(0deg) !important; }
}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);
}
.sa {
display: grid;
justify-items: center;
gap: 12px;
width: 100%;
}
.sa__art {
display: block;
width: clamp(176px, 60vw, 320px);
height: auto;
}
.sa__bit {
transform-box: fill-box;
transform-origin: 50% 50%;
}
.sa__bit--a {
fill: #17141a;
}
.sa__bit--b {
fill: #2f6df6;
}
.sa__bit--c {
fill: #fff7e6;
}
.sa__bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
}
.sa__seed {
margin: 0;
display: inline-flex;
align-items: center;
gap: 4px;
color: rgba(23, 20, 26, 0.78);
font-weight: 700;
font-size: clamp(9px, 2.4vw, 11px);
}
.sa__btn {
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
border-radius: 999px;
padding: 4px 12px;
background: #17141a;
color: #fff7e6;
font-family: inherit;
font-weight: 700;
font-size: clamp(9px, 2.4vw, 11px);
cursor: pointer;
}
.sa__btn:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 2px;
}
.sa__ic {
width: 12px;
height: 12px;
flex: none;
}
.sa.is-demo .sa__bit {
animation-name: saPulse;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * -124ms);
}
.sa.is-draw .sa__bit {
animation-name: saStamp;
animation-duration: 600ms;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: 1;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 28ms);
}
@keyframes saPulse {
0% {
transform: scale(1) rotate(0deg);
}
28% {
transform: scale(0.5) rotate(-18deg);
}
56% {
transform: scale(1.08) rotate(6deg);
}
80% {
transform: scale(1) rotate(0deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
@keyframes saStamp {
from {
transform: scale(0.2) rotate(-30deg);
opacity: 0;
}
to {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
@media (max-width: 340px) {
.sa {
gap: 8px;
}
.sa__ic {
width: 10px;
height: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.sa.is-demo .sa__bit,
.sa.is-draw .sa__bit {
animation: none !important;
transform: scale(1) rotate(0deg) !important;
}
}var root = document.getElementById('root');
var art = document.getElementById('art');
var num = document.getElementById('num');
var COLS = 12, ROWS = 8, CELL = 20, HALF = 6;
var NAMES = ['godrich', 'aurora', 'bluebird', 'studio', 'maker'];
var TONES = ['a', 'b', 'c'];
var at = 0;
// mulberry32 — 32비트 정수 Seed 하나로 언제나 같은 난수열을 돌려준다. 재현 가능한 생성의 뿌리
function mulberry32(a) {
return function () {
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;
};
}
// FNV-1a: 글자 하나씩 섞어 32비트 정수 하나로 접는다. 같은 글자열은 언제나 같은 숫자가 된다
function hash(text) {
var h = 2166136261, i;
for (i = 0; i < text.length; i++) {
h = Math.imul(h ^ text.charCodeAt(i), 16777619);
}
return h >>> 0;
}
// 반쪽만 뽑고 오른쪽은 거울처럼 되비춘다 — 좌우 대칭이라야 얼굴처럼 읽힌다
function shape(kind, x, y, tone, order) {
var c = ' class="sa__bit sa__bit--' + tone + '" style="--i:' + order + '"';
if (kind === 1) { return '<rect' + c + ' x="' + (x + 3) + '" y="' + (y + 3) + '" width="14" height="14" rx="4"/>'; }
if (kind === 2) { return '<circle' + c + ' cx="' + (x + 10) + '" cy="' + (y + 10) + '" r="7.5"/>'; }
return '<polygon' + c + ' points="' + (x + 10) + ',' + (y + 2) + ' ' + (x + 18) + ',' + (y + 18) +
' ' + (x + 2) + ',' + (y + 18) + '"/>';
}
function build() {
var name = NAMES[at], rand = mulberry32(hash(name)), parts = [], col, row;
for (row = 0; row < ROWS; row++) {
for (col = 0; col < HALF; col++) {
var draw = rand(), kind = 1 + Math.floor(rand() * 3), tone = TONES[Math.floor(rand() * 3)];
if (draw < 0.38) { continue; }
var order = col + row;
parts.push(shape(kind, col * CELL, row * CELL - 8, tone, order));
parts.push(shape(kind, (COLS - 1 - col) * CELL, row * CELL - 8, tone, order));
}
}
art.innerHTML = parts.join('');
art.setAttribute('aria-label', art.getAttribute('data-label').replace('{n}', name));
num.textContent = name;
}
document.getElementById('again').addEventListener('click', function () {
at = (at + 1) % NAMES.length;
build();
root.classList.remove('is-demo');
root.classList.remove('is-draw');
void art.getBoundingClientRect().width; // 한 번 읽어 리플로를 일으켜야 같은 애니메이션이 처음부터 다시 돈다
root.classList.add('is-draw');
});
var copyBtn = document.getElementById('copy');
copyBtn.addEventListener('click', function () {
var label = copyBtn.querySelector('.sa__label');
var before = label.textContent;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(art.outerHTML).catch(function () {});
}
label.textContent = copyBtn.getAttribute('data-done');
setTimeout(function () { label.textContent = before; }, 1200);
});
build();// SeedAvatarBackground — vanilla 판(run/293/demo/09-generative-seed-avatar-bg/index.html) 의 React 포트.
// 시드가 숫자가 아니라 글자열이다. FNV-1a 로 접어 얻은 32비트 정수를 그대로 난수 시드로 쓰면
// 같은 이름은 언제나 같은 무늬로 돌아온다. 격자 수·칸 크기·이름 목록은 vanilla 와 같은 값이다.
import { useMemo, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./SeedAvatarBackground.module.scss";
export type SeedAvatarBackgroundProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
seedText?: string;
regenerateText?: string;
copyText?: string;
copiedText?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
// useRef 는 T | null 을 돌려준다. 처음부터 값이 있고 절대 비지 않는 상자는 이렇게 좁혀 쓴다
type Box<T> = { current: T };
// 조각 하나 — 모양(1 네모 · 2 원 · 그 밖 삼각형)·왼쪽 위 좌표·색 계열·대각선 차례
type Bit = { kind: number; x: number; y: number; tone: string; order: number };
const COLS = 12;
const ROWS = 8;
const CELL = 20;
const HALF = 6;
const NAMES: string[] = ["godrich", "aurora", "bluebird", "studio", "maker"];
const TONES: string[] = ["a", "b", "c"];
// mulberry32 — 32비트 정수 시드 하나로 언제나 같은 난수열을 돌려준다. 재현 가능한 생성의 뿌리
function mulberry32(a: number): () => number {
return function (): number {
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;
};
}
// FNV-1a: 글자 하나씩 섞어 32비트 정수 하나로 접는다. 같은 글자열은 언제나 같은 숫자가 된다
function hash(text: string): number {
let h = 2166136261;
for (let i = 0; i < text.length; i++) {
h = Math.imul(h ^ text.charCodeAt(i), 16777619);
}
return h >>> 0;
}
// 반쪽(HALF 열)만 뽑고 오른쪽은 거울처럼 되비춘다 — 좌우 대칭이라야 얼굴처럼 읽힌다.
// 건너뛰는 칸에서도 난수를 세 번 다 뽑는다 (vanilla 와 같은 순서라야 같은 이름에 같은 무늬가 나온다)
function build(name: string): Bit[] {
const rand = mulberry32(hash(name));
const parts: Bit[] = [];
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < HALF; col++) {
const draw = rand();
const kind = 1 + Math.floor(rand() * 3);
const tone = TONES[Math.floor(rand() * 3)];
if (draw < 0.38) { continue; }
const order = col + row;
parts.push({ kind: kind, x: col * CELL, y: row * CELL - 8, tone: tone, order: order });
parts.push({ kind: kind, x: (COLS - 1 - col) * CELL, y: row * CELL - 8, tone: tone, order: order });
}
}
return parts;
}
// 색 계열은 문자열 하나지만 CSS 모듈 클래스는 객체 속성이라 이렇게 갈라 준다 (동적 색인 금지)
function toneClass(tone: string): string {
if (tone === "a") { return styles.bitA; }
if (tone === "b") { return styles.bitB; }
return styles.bitC;
}
function renderBit(b: Bit, key: string) {
const cls = `${styles.bit} ${toneClass(b.tone)}`;
const vars = { "--i": b.order } as CSSProperties;
if (b.kind === 1) {
return <rect key={key} className={cls} style={vars} x={b.x + 3} y={b.y + 3} width={14} height={14} rx={4} />;
}
if (b.kind === 2) {
return <circle key={key} className={cls} style={vars} cx={b.x + 10} cy={b.y + 10} r={7.5} />;
}
const points = `${b.x + 10},${b.y + 2} ${b.x + 18},${b.y + 18} ${b.x + 2},${b.y + 18}`;
return <polygon key={key} className={cls} style={vars} points={points} />;
}
export default function SeedAvatarBackground({
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#17141a",
label = "A left-right symmetric background folded out of a name, seed {n}",
seedText = "Seed",
regenerateText = "Regenerate",
copyText = "Copy SVG",
copiedText = "Copied",
}: SeedAvatarBackgroundProps) {
const artRef = useRef<SVGSVGElement | null>(null);
const timerRef = useRef<number>(0) as Box<number>;
const [nameIndex, setNameIndex] = useState(0);
const [mode, setMode] = useState("demo");
const [copyLabel, setCopyLabel] = useState(copyText);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const name = NAMES[nameIndex];
// 이름이 바뀔 때만 다시 계산한다 — 같은 이름이면 언제나 같은 무늬가 나온다
const bits = useMemo(() => build(NAMES[nameIndex]), [nameIndex]);
function regenerate(): void {
setNameIndex((prev: number) => (prev + 1) % NAMES.length);
setMode("draw");
}
function copySvg(): void {
const art = artRef.current;
if (!art) { return; }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(art.outerHTML).catch(function () {});
}
setCopyLabel(copiedText);
clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => setCopyLabel(copyText), 1200);
}
const rootClass = `${styles.sa} ${mode === "demo" ? styles.isDemo : styles.isDraw}`;
return (
<div className={rootClass} style={vars}>
<svg
ref={artRef}
className={styles.art}
viewBox="0 0 240 144"
role="img"
aria-label={label.replace("{n}", name)}
>
{bits.map((b: Bit, i: number) => renderBit(b, String(i)))}
</svg>
<div className={styles.bar}>
<p className={styles.seed}>{seedText} <b>{name}</b></p>
<button className={styles.btn} type="button" onClick={regenerate}>{regenerateText}</button>
<button className={styles.btn} type="button" onClick={copySvg}>{copyLabel}</button>
</div>
</div>
);
}09 Background made from a name — Measured area 11.025% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · CSS Background Pattern Generator: 9 Seeded Scenes
The post this demo lives in
CSS Background Pattern Generator: 9 Seeded ScenesA CSS background pattern generator turns one number into a drawing instead of an image file. Feed these nine the same se See all demos in this post


