ORDER#1042
Tote bag20.00
Shipping4.00
TOTAL$24.00
PAIDCARD 4242
import { useRef, useState, type CSSProperties } from 'react';
type P = [number, number];
const UNIT = 25;
const S2 = Math.SQRT2;
// The seven pieces at their own size. Triangles list the long side first, so a
// target polygon can be matched to its piece by its first edge.
const BASE: P[][] = [
[[0, 0], [4, 0], [2, 2]],
[[0, 0], [4, 0], [2, 2]],
[[0, 0], [2 * S2, 0], [S2, S2]],
[[0, 0], [2, 0], [1, 1]],
[[0, 0], [2, 0], [1, 1]],
[[0, 0], [S2, 0], [S2, S2], [0, S2]],
[[0, 0], [2, 0], [3, 1], [1, 1]],
];
const FIGURES: { name: string; pieces: P[][] }[] = [
{
name: 'Square',
pieces: [
[[0, 0], [4, 0], [2, 2]],
[[0, 4], [0, 0], [2, 2]],
[[2, 4], [4, 2], [4, 4]],
[[4, 0], [4, 2], [3, 1]],
[[1, 3], [3, 3], [2, 2]],
[[2, 2], [3, 1], [4, 2], [3, 3]],
[[0, 4], [2, 4], [3, 3], [1, 3]],
],
},
{
name: 'Sailboat',
pieces: [
[[0.06, -2.83], [2.89, 0], [0.06, 0]],
[[-2, 0.16], [2, 0.16], [0, 2.16]],
[[-0.06, -2], [-2.06, 0], [-0.06, 0]],
[[-3.2, 3.3], [-1.2, 3.3], [-2.2, 2.3]],
[[1.2, 3.3], [3.2, 3.3], [2.2, 2.3]],
[[-2.4, -3.4], [-1.4, -2.4], [-2.4, -1.4], [-3.4, -2.4]],
[[-1, 3.3], [1, 3.3], [2, 2.3], [0, 2.3]],
],
},
{
name: 'Cat',
pieces: [
[[-1, 1.05], [-1, 5.05], [1, 3.05]],
[[3, 5.05], [-1, 5.05], [1, 3.05]],
[[-1.05, 3.05], [-3.05, 5.05], [-1.05, 5.05]],
[[-2, 0], [-1, -1], [-2, -1]],
[[-1, -1], [0, 0], [0, -1]],
[[-1, -1], [0, 0], [-1, 1], [-2, 0]],
[[3.05, 5.05], [4.464, 3.636], [4.464, 2.222], [3.05, 3.636]],
],
},
{
name: 'Fish',
pieces: [
[[0, 2], [0, -2], [2, 0]],
[[0, -2], [0, 2], [-2, 0]],
[[-3.42, -1.42], [-3.42, 1.42], [-2, 0]],
[[0, -2.05], [-1.41, -0.64], [-1.41, -2.05]],
[[-1.41, 0.64], [0, 2.05], [-1.41, 2.05]],
[[2.6, -2.4], [3.6, -1.4], [2.6, -0.4], [1.6, -1.4]],
[[1.4, 2], [3.4, 2], [4.4, 1], [2.4, 1]],
],
},
];
const centroid = (poly: P[]): P => {
const sum = poly.reduce<P>((acc, [x, y]) => [acc[0] + x, acc[1] + y], [0, 0]);
return [sum[0] / poly.length, sum[1] / poly.length];
};
const area = (poly: P[]) => poly.reduce((acc, [x, y], i) => {
const [nx, ny] = poly[(i + 1) % poly.length];
return acc + x * ny - nx * y;
}, 0);
// Where each piece sits in each figure: position (centred on the figure), turn and flip.
const POSES = FIGURES.map(({ pieces }) => {
const all = pieces.flat();
const mx = (Math.min(...all.map((p) => p[0])) + Math.max(...all.map((p) => p[0]))) / 2;
const my = (Math.min(...all.map((p) => p[1])) + Math.max(...all.map((p) => p[1]))) / 2;
return pieces.map((poly, i) => {
const base = BASE[i];
const [cx, cy] = centroid(poly);
const flip = Math.sign(area(poly)) !== Math.sign(area(base));
const ax = poly[1][0] - poly[0][0];
const ay = poly[1][1] - poly[0][1];
return {
x: (cx - mx) * UNIT,
y: (cy - my) * UNIT,
rot: (Math.atan2(ay, ax) * 180) / Math.PI,
flip,
};
});
});
const SHAPES = BASE.map((poly) => {
const [cx, cy] = centroid(poly);
return poly.map(([x, y]) => `${((x - cx) * UNIT).toFixed(2)},${((y - cy) * UNIT).toFixed(2)}`).join(' ');
});
const TONES = ['--kit-100', '--kit-500', '--kit-300', '--kit-500', '--kit-200', '--kit-400', '--kit-300'];
const css = `
.btn-tangram { flex: none; --k: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 4px 10px 6px; border: 0; border-radius: 14px; background: none; color: light-dark(#373a40, #f1f2f3); font: inherit; cursor: pointer; }
.btn-tangram:hover, .btn-tangram:focus-visible { --k: 1.07; }
.btn-tangram:active { --k: 0.96; }
.btn-tangram:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.btn-tangram svg { display: block; width: 240px; height: 186px; overflow: visible; }
.btn-tangram__piece { stroke: light-dark(#fbfcfd, #121417); stroke-width: 1.4; stroke-linejoin: round; transform: translate(calc(var(--x) * var(--k)), calc(var(--y) * var(--k))) rotate(var(--rot)) scale(1, var(--flip)); transition: transform 0.72s cubic-bezier(0.3, 1.3, 0.5, 1) calc(var(--i) * 38ms); }
.btn-tangram__label { display: flex; align-items: baseline; gap: 7px; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }
.btn-tangram__name { color: light-dark(#5f626a, #a7abb2); font-size: 12px; font-weight: 500; }
@media (prefers-reduced-motion: reduce) {
.btn-tangram__piece { transition: none; }
}
`;
export function TangramButton() {
const [figure, setFigure] = useState(0);
// Turns accumulate, so a piece always takes the short way to its next angle.
const turns = useRef(POSES[0].map((p) => p.rot));
const poses = POSES[figure];
poses.forEach((p, i) => {
const delta = ((p.rot - turns.current[i] + 540) % 360) - 180;
turns.current[i] += delta;
});
return (
<>
<style>
{css}
</style>
<button type="button" className="btn-tangram" aria-label={`Shuffle the shape. Now showing: ${FIGURES[figure].name}`} onClick={() => setFigure((f) => (f + 1) % FIGURES.length)}>
<svg viewBox="-120 -93 240 186" aria-hidden="true">
{SHAPES.map((points, i) => (
<polygon
key={i}
className="btn-tangram__piece"
points={points}
fill={`var(${TONES[i]})`}
style={{ '--i': i, '--x': `${poses[i].x.toFixed(2)}px`, '--y': `${poses[i].y.toFixed(2)}px`, '--rot': `${turns.current[i].toFixed(2)}deg`, '--flip': poses[i].flip ? -1 : 1 } as CSSProperties}
/>
))}
</svg>
<span className="btn-tangram__label">
Shuffle <span className="btn-tangram__name">{FIGURES[figure].name}</span>
</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnTangram(el) on the mounted element instead.
<button type="button" class="btn-tangram" aria-label="Shuffle the shape. Now showing: Square">
<svg viewBox="-120 -93 240 186" aria-hidden="true">
<polygon class="btn-tangram__piece" points="-50.00,-16.67 50.00,-16.67 0.00,33.33" fill="light-dark(#373a40, #f1f2f3)" style="--i:0;--x:0.00px;--y:-33.33px;--rot:0.00deg;--flip:1"></polygon>
<polygon class="btn-tangram__piece" points="-50.00,-16.67 50.00,-16.67 0.00,33.33" fill="light-dark(#6d7179, #7d8087)" style="--i:1;--x:-33.33px;--y:0.00px;--rot:-90.00deg;--flip:1"></polygon>
<polygon class="btn-tangram__piece" points="-35.36,-11.79 35.36,-11.79 0.00,23.57" fill="light-dark(#50535b, #c6c8cd)" style="--i:2;--x:33.33px;--y:33.33px;--rot:-45.00deg;--flip:1"></polygon>
<polygon class="btn-tangram__piece" points="-25.00,-8.33 25.00,-8.33 0.00,16.67" fill="light-dark(#6d7179, #7d8087)" style="--i:3;--x:41.67px;--y:-25.00px;--rot:90.00deg;--flip:1"></polygon>
<polygon class="btn-tangram__piece" points="-25.00,-8.33 25.00,-8.33 0.00,16.67" fill="light-dark(#42454c, #e5e6e9)" style="--i:4;--x:0.00px;--y:16.67px;--rot:0.00deg;--flip:-1"></polygon>
<polygon class="btn-tangram__piece" points="-17.68,-17.68 17.68,-17.68 17.68,17.68 -17.68,17.68" fill="light-dark(#5f626a, #a7abb2)" style="--i:5;--x:25.00px;--y:0.00px;--rot:-45.00deg;--flip:1"></polygon>
<polygon class="btn-tangram__piece" points="-37.50,-12.50 12.50,-12.50 37.50,12.50 -12.50,12.50" fill="light-dark(#50535b, #c6c8cd)" style="--i:6;--x:-12.50px;--y:37.50px;--rot:0.00deg;--flip:-1"></polygon>
</svg>
<span class="btn-tangram__label">
Shuffle
<span class="btn-tangram__name">Square</span>
</span>
</button>.btn-tangram { flex: none; --k: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 4px 10px 6px; border: 0; border-radius: 14px; background: none; color: light-dark(#373a40, #f1f2f3); font: inherit; cursor: pointer; }
.btn-tangram:hover, .btn-tangram:focus-visible { --k: 1.07; }
.btn-tangram:active { --k: 0.96; }
.btn-tangram:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.btn-tangram svg { display: block; width: 240px; height: 186px; overflow: visible; }
.btn-tangram__piece { stroke: light-dark(#fbfcfd, #121417); stroke-width: 1.4; stroke-linejoin: round; transform: translate(calc(var(--x) * var(--k)), calc(var(--y) * var(--k))) rotate(var(--rot)) scale(1, var(--flip)); transition: transform 0.72s cubic-bezier(0.3, 1.3, 0.5, 1) calc(var(--i) * 38ms); }
.btn-tangram__label { display: flex; align-items: baseline; gap: 7px; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }
.btn-tangram__name { color: light-dark(#5f626a, #a7abb2); font-size: 12px; font-weight: 500; }
@media (prefers-reduced-motion: reduce) {
.btn-tangram__piece { transition: none; }
}function initBtnTangram(root) {
var UNIT = 25;
var S2 = Math.SQRT2;
// The seven pieces at their own size. Triangles list the long side first, so a
// target polygon can be matched to its piece by its first edge.
var BASE = [
[[0, 0], [4, 0], [2, 2]],
[[0, 0], [4, 0], [2, 2]],
[[0, 0], [2 * S2, 0], [S2, S2]],
[[0, 0], [2, 0], [1, 1]],
[[0, 0], [2, 0], [1, 1]],
[[0, 0], [S2, 0], [S2, S2], [0, S2]],
[[0, 0], [2, 0], [3, 1], [1, 1]],
];
var FIGURES = [
{ name: 'Square', pieces: [[[0, 0], [4, 0], [2, 2]], [[0, 4], [0, 0], [2, 2]], [[2, 4], [4, 2], [4, 4]], [[4, 0], [4, 2], [3, 1]], [[1, 3], [3, 3], [2, 2]], [[2, 2], [3, 1], [4, 2], [3, 3]], [[0, 4], [2, 4], [3, 3], [1, 3]]] },
{ name: 'Sailboat', pieces: [[[0.06, -2.83], [2.89, 0], [0.06, 0]], [[-2, 0.16], [2, 0.16], [0, 2.16]], [[-0.06, -2], [-2.06, 0], [-0.06, 0]], [[-3.2, 3.3], [-1.2, 3.3], [-2.2, 2.3]], [[1.2, 3.3], [3.2, 3.3], [2.2, 2.3]], [[-2.4, -3.4], [-1.4, -2.4], [-2.4, -1.4], [-3.4, -2.4]], [[-1, 3.3], [1, 3.3], [2, 2.3], [0, 2.3]]] },
{ name: 'Cat', pieces: [[[-1, 1.05], [-1, 5.05], [1, 3.05]], [[3, 5.05], [-1, 5.05], [1, 3.05]], [[-1.05, 3.05], [-3.05, 5.05], [-1.05, 5.05]], [[-2, 0], [-1, -1], [-2, -1]], [[-1, -1], [0, 0], [0, -1]], [[-1, -1], [0, 0], [-1, 1], [-2, 0]], [[3.05, 5.05], [4.464, 3.636], [4.464, 2.222], [3.05, 3.636]]] },
{ name: 'Fish', pieces: [[[0, 2], [0, -2], [2, 0]], [[0, -2], [0, 2], [-2, 0]], [[-3.42, -1.42], [-3.42, 1.42], [-2, 0]], [[0, -2.05], [-1.41, -0.64], [-1.41, -2.05]], [[-1.41, 0.64], [0, 2.05], [-1.41, 2.05]], [[2.6, -2.4], [3.6, -1.4], [2.6, -0.4], [1.6, -1.4]], [[1.4, 2], [3.4, 2], [4.4, 1], [2.4, 1]]] },
];
var nodes = root.querySelectorAll('.btn-tangram__piece');
var name = root.querySelector('.btn-tangram__name');
var figure = 0;
function centroid(poly) {
var x = 0;
var y = 0;
poly.forEach(function (p) {
x += p[0];
y += p[1];
});
return [x / poly.length, y / poly.length];
}
function area(poly) {
return poly.reduce(function (acc, p, i) {
var n = poly[(i + 1) % poly.length];
return acc + p[0] * n[1] - n[0] * p[1];
}, 0);
}
// Where each piece sits in each figure: position (centred on the figure), turn and flip.
var POSES = FIGURES.map(function (f) {
var all = [].concat.apply([], f.pieces);
var xs = all.map(function (p) { return p[0]; });
var ys = all.map(function (p) { return p[1]; });
var mx = (Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2;
var my = (Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2;
return f.pieces.map(function (poly, i) {
var c = centroid(poly);
return {
x: (c[0] - mx) * UNIT,
y: (c[1] - my) * UNIT,
rot: (Math.atan2(poly[1][1] - poly[0][1], poly[1][0] - poly[0][0]) * 180) / Math.PI,
flip: Math.sign(area(poly)) !== Math.sign(area(BASE[i])),
};
});
});
// Turns accumulate, so a piece always takes the short way to its next angle.
var turns = POSES[0].map(function (p) {
return p.rot;
});
function onClick() {
figure = (figure + 1) % FIGURES.length;
POSES[figure].forEach(function (p, i) {
turns[i] += ((p.rot - turns[i] + 540) % 360) - 180;
var s = nodes[i].style;
s.setProperty('--x', p.x.toFixed(2) + 'px');
s.setProperty('--y', p.y.toFixed(2) + 'px');
s.setProperty('--rot', turns[i].toFixed(2) + 'deg');
s.setProperty('--flip', p.flip ? -1 : 1);
});
name.textContent = FIGURES[figure].name;
root.setAttribute('aria-label', 'Shuffle the shape. Now showing: ' + FIGURES[figure].name);
}
root.addEventListener('click', onClick);
return function () {
root.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.btn-tangram').forEach(initBtnTangram);Goal: Create a "Tangram" button.
Visual behavior: Seven solid pieces that slide and turn into a new picture on every press: a square, a sailboat, a cat, a fish. The button changes its whole silhouette.
Technique:
- Each figure is written as plain polygons. Position, turn and flip for every piece are worked out by comparing its polygon with the piece at rest, so a new figure is only a list of points.
- The pieces are moved with a CSS transition built from variables and a small stagger. Turns accumulate so each piece takes the short way round, and hovering multiplies every position slightly to loosen the shape.
Accessibility: A real button whose aria-label names the shape on show; the pieces are aria-hidden. Under prefers-reduced-motion the pieces jump to the new shape.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Tangram" button.
Visual behavior: Seven solid pieces that slide and turn into a new picture on every press: a square, a sailboat, a cat, a fish. The button changes its whole silhouette.
Technique:
- Each figure is written as plain polygons. Position, turn and flip for every piece are worked out by comparing its polygon with the piece at rest, so a new figure is only a list of points.
- The pieces are moved with a CSS transition built from variables and a small stagger. Turns accumulate so each piece takes the short way round, and hovering multiplies every position slightly to loosen the shape.
Accessibility: A real button whose aria-label names the shape on show; the pieces are aria-hidden. Under prefers-reduced-motion the pieces jump to the new shape.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
Seven solid pieces that slide and turn into a new picture on every press: a square, a sailboat, a cat, a fish. The button changes its whole silhouette.
A real button whose aria-label names the shape on show; the pieces are aria-hidden. Under prefers-reduced-motion the pieces jump to the new shape.