<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>拍立得头像</title>
<style>
:root {
--paper: #fbf8f1;
--paper-2: #f3eee2;
--card: #fffdf8;
--ink: #1c2230;
--ink-2: #4a5163;
--ink-3: #636877;
--line: #e6dfd0;
--dot: #d8d0bf;
--blue: #2f5bd3;
--seal: #c0391f;
/* The photo paper stays white in both themes, like a real print. */
--sheet: #fffdf8;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
/* A damped spring sampled into linear(): overshoots about 16%, then settles. */
--spring: linear(
0,
0.062,
0.215,
0.411,
0.615,
0.799,
0.95,
1.059,
1.126,
1.158,
1.161,
1.145,
1.116,
1.083,
1.051,
1.023,
1.001,
0.986,
0.977,
0.974,
0.974,
0.978,
0.983,
0.988,
0.994,
0.998,
1.001,
1.003,
1.004,
1.004,
1
);
--f-sans: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
--f-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
/* Hand lettering from system fonts only: Kaiti on Mac / Windows, AR PL UKai on Linux. */
--f-hand: "Kaiti SC", STKaiti, KaiTi, "AR PL UKai CN", "Segoe Print", "Bradley Hand", cursive;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #14161b;
--paper-2: #1c1f26;
--card: #1e2129;
--ink: #ece7dc;
--ink-2: #bab6ac;
--ink-3: #959aa8;
--line: #31353f;
--dot: #2c3039;
--blue: #8eaaff;
--seal: #f0694e;
color-scheme: dark;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--paper) radial-gradient(var(--dot) 1px, transparent 1.2px) 0 0 / 22px 22px;
color: var(--ink);
font: 16px/1.7 var(--f-sans);
}
/* ---------- the desk the photo lies on ---------- */
.desk {
position: relative;
height: 100vh;
min-height: 440px;
overflow: hidden;
}
.hint {
position: absolute;
left: 16px;
right: 16px;
bottom: 16px;
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
align-items: center;
justify-content: center;
font: 400 12px/1.5 var(--f-mono);
color: var(--ink-3);
}
.hint button {
font: 500 12px/1 var(--f-mono);
color: var(--ink-2);
padding: 7px 11px;
border: 1px dashed var(--ink-3);
border-radius: 999px;
background: transparent;
cursor: pointer;
transition: scale 0.45s var(--spring);
}
.hint button:hover {
border-style: solid;
color: var(--ink);
background: var(--card);
}
.hint button:active {
scale: 0.93;
}
.doodle {
position: absolute;
pointer-events: none;
color: var(--seal);
}
.doodle text {
font: 400 22px var(--f-hand);
fill: currentColor;
}
/* ---------- the polaroid ---------- */
.polaroid {
position: absolute;
width: clamp(160px, 20vw, 260px);
margin: 0;
padding: 12px 12px 0;
background: var(--sheet);
border-radius: 3px;
box-shadow:
0 1px 0 rgba(28, 34, 48, 0.05),
0 16px 30px -16px rgba(28, 34, 48, 0.45);
transform: rotate(4deg); /* resting tilt; `rotate` below adds the drag lean on top */
touch-action: none;
user-select: none;
cursor: grab;
transition:
scale 0.55s var(--spring),
rotate 0.7s var(--spring),
box-shadow 0.25s var(--ease);
}
.polaroid:hover {
scale: 1.04;
}
.polaroid.dragging {
cursor: grabbing;
scale: 1.035;
rotate: calc(var(--tilt, 0) * 1deg);
box-shadow: 0 24px 48px -18px rgba(28, 34, 48, 0.45);
}
.polaroid:focus-visible {
outline: 2px dashed var(--blue);
outline-offset: 6px;
}
.polaroid svg {
display: block;
width: 100%;
height: auto;
border-radius: 1px;
}
.polaroid figcaption {
padding: 8px 2px 10px;
text-align: center;
font: 400 clamp(18px, 1.9vw, 24px) / 1.2 var(--f-hand);
color: #2b3242; /* fixed: ink on white photo paper in both themes */
}
/* A strip of washi tape: two stripes of the same yellow at different strengths. */
.tape {
--t: 255, 214, 61;
position: absolute;
top: -9px;
left: 50%;
width: 78px;
height: 18px;
transform: translateX(-50%) rotate(-3deg);
background: repeating-linear-gradient(90deg, rgba(var(--t), 0.9) 0 5px, rgba(var(--t), 0.45) 5px 10px);
border-radius: 1px;
}
@media (max-width: 520px) {
.polaroid {
padding: 10px 10px 0;
}
}
@media (prefers-reduced-motion: reduce) {
.polaroid,
.hint button {
transition: none;
}
}
</style>
</head>
<body>
<main class="desk" id="desk">
<svg class="doodle" id="doodle" width="150" height="86" viewBox="0 0 150 86" aria-hidden="true">
<text x="2" y="20">拖我试试</text>
<path
d="M20 32 C 50 44, 92 46, 128 74"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
/>
<path
d="M114 76 L129 75 L124 61"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<figure class="polaroid" id="photo" tabindex="0" aria-label="拍立得头像,可拖动;也可以用方向键移动">
<span class="tape" aria-hidden="true"></span>
<!-- Placeholder portrait: a pencil doodle drawn in SVG. Swap in your own <img>. -->
<svg viewBox="0 0 200 200" role="img" aria-label="铅笔涂鸦的小人头像">
<defs>
<radialGradient id="bg" cx="50%" cy="38%" r="75%">
<stop offset="0" stop-color="#f4f2ec" />
<stop offset="1" stop-color="#d9d5cb" />
</radialGradient>
<!-- A little wobble so the lines look drawn by hand, not by a ruler. -->
<filter id="pencil" x="-5%" y="-5%" width="110%" height="110%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="3" />
<feDisplacementMap in="SourceGraphic" scale="2.2" />
</filter>
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(35)">
<line x1="0" y1="0" x2="0" y2="6" stroke="#5a5f6b" stroke-width="1" opacity="0.35" />
</pattern>
</defs>
<rect width="200" height="200" fill="url(#bg)" />
<g
fill="none"
stroke="#3d424e"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#pencil)"
>
<!-- shoulders and collar -->
<path d="M22 204 C 30 168, 62 152, 100 152 C 138 152, 170 168, 178 204" />
<path d="M84 152 L100 172 L116 152" />
<!-- neck -->
<path d="M88 132 L87 152 M112 132 L113 152" />
<!-- face, drawn twice slightly off like a quick sketch -->
<path
d="M58 92 C 56 54, 80 40, 100 40 C 124 40, 144 56, 142 92 C 140 122, 122 140, 100 140 C 78 140, 60 122, 58 92 Z"
/>
<path
d="M60 94 C 58 58, 82 43, 101 42 C 122 42, 141 58, 140 94 C 138 120, 121 138, 99 138"
stroke-width="1"
opacity="0.55"
/>
<!-- hair: a scribbled fringe -->
<path
d="M56 84 C 52 52, 74 30, 102 30 C 130 30, 150 50, 144 84 C 138 66, 128 58, 118 56 C 108 66, 88 70, 70 64 C 64 70, 60 76, 56 84 Z"
fill="url(#hatch)"
/>
<path d="M74 50 C 86 42, 104 40, 122 46 M68 60 C 84 52, 112 50, 134 60" stroke-width="1.2" opacity="0.6" />
<!-- brows, eyes, nose, smile -->
<path d="M74 84 C 80 80, 88 80, 92 83 M108 83 C 113 80, 121 80, 126 84" />
<path d="M80 96 C 82 92, 88 92, 90 96 M110 96 C 112 92, 118 92, 120 96" />
<path d="M101 100 C 99 108, 97 112, 103 113" stroke-width="1.6" />
<path d="M86 122 C 94 128, 108 128, 115 121" />
<!-- cheek shading -->
<path d="M66 108 C 70 118, 76 126, 84 132" stroke="url(#hatch)" stroke-width="10" opacity="0.8" />
</g>
</svg>
<figcaption>你好,我是小王</figcaption>
</figure>
<p class="hint">
<span>拖拽或方向键移动 · 拖得越快歪得越多</span>
<button type="button" id="reset">放回原处</button>
</p>
</main>
<script>
const desk = document.getElementById("desk");
const photo = document.getElementById("photo");
const doodle = document.getElementById("doodle");
// Where the photo rests: a bit right of centre, with the hand-written arrow pointing at it.
function home() {
const wide = desk.clientWidth > 600;
const x = Math.round(desk.clientWidth / 2 - photo.offsetWidth / 2 + (wide ? 90 : 24));
const y = Math.max(wide ? 40 : 96, Math.round(desk.clientHeight / 2 - photo.offsetHeight / 2 - 24));
place(x, y);
doodle.style.left = `${Math.max(4, x - 150)}px`;
doodle.style.top = `${Math.max(6, y - 40)}px`;
}
function place(x, y) {
const maxX = desk.clientWidth - photo.offsetWidth;
const maxY = desk.clientHeight - photo.offsetHeight;
photo.style.left = `${Math.max(0, Math.min(maxX, x))}px`;
photo.style.top = `${Math.max(0, Math.min(maxY, y))}px`;
}
home();
addEventListener("resize", () => place(photo.offsetLeft, photo.offsetTop));
document.getElementById("reset").addEventListener("click", home);
// Drag: pick it up (it grows a little), lean into the direction it moves (the faster sideways,
// the more it tilts), let go and the CSS spring settles it flat again.
photo.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;
const startX = e.clientX;
const startY = e.clientY;
const ox = photo.offsetLeft;
const oy = photo.offsetTop;
let moved = false;
let lastX = e.clientX;
let lastT = e.timeStamp;
let tilt = 0;
photo.setPointerCapture(e.pointerId);
const move = (ev) => {
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
if (!moved && Math.hypot(dx, dy) < 4) return; // a click is not a drag
moved = true;
photo.classList.add("dragging");
const dt = Math.max(8, ev.timeStamp - lastT);
tilt = tilt * 0.7 + Math.max(-9, Math.min(9, ((ev.clientX - lastX) / dt) * 6)) * 0.3;
lastX = ev.clientX;
lastT = ev.timeStamp;
photo.style.setProperty("--tilt", tilt.toFixed(2));
place(ox + dx, oy + dy);
};
const up = () => {
photo.removeEventListener("pointermove", move);
photo.classList.remove("dragging");
photo.style.removeProperty("--tilt");
};
photo.addEventListener("pointermove", move);
photo.addEventListener("pointerup", up, { once: true });
photo.addEventListener("pointercancel", up, { once: true });
});
// Keyboard: arrow keys nudge it (Shift for bigger steps).
photo.addEventListener("keydown", (e) => {
const step = e.shiftKey ? 40 : 10;
const d = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] }[e.key];
if (!d) return;
e.preventDefault();
place(photo.offsetLeft + d[0], photo.offsetTop + d[1]);
});
</script>
</body>
</html>