/* global React */
/* The signature "live design playground" — drag stickers, type, shapes, swap bg */
const { useState: lcUseState, useRef: lcUseRef, useEffect: lcUseEffect } = React;
const LC_BG_OPTIONS = [
{ name: "PAPER", val: "var(--paper-2)" },
{ name: "ACID", val: "var(--acid)" },
{ name: "PINK", val: "var(--pink)" },
{ name: "BLUE", val: "var(--blue)" },
{ name: "MINT", val: "var(--mint)" },
{ name: "LILAC", val: "var(--lilac)" },
{ name: "INK", val: "var(--ink)" },
];
const LC_STICKERS = [
"MORE COWBELL", "NEW!", "AS SEEN ON YOUR FYP", "SHIP IT", "★ TASTY", "MADE IN INDIA", "DO IT LOUDER", "BIG IF TRUE",
];
const LC_EMOJIS = ["★","✺","✦","◐","◯","❍","♥","⚡","☀","☻"];
const LC_PHRASES = ["LOUDER.", "BOLDER.", "RAW.", "weird.", "FAST.", "honest."];
const LC_SHAPE_COLORS = ["var(--pink)","var(--acid)","var(--blue)","var(--mint)","var(--lava)","var(--lilac)"];
function uid() { return Math.random().toString(36).slice(2, 9); }
function initialItems(stageW = 900, stageH = 560) {
// Layout items as a percentage of the stage so they fit any viewport.
const px = (rx, ry) => ({ x: Math.round(rx * stageW), y: Math.round(ry * stageH) });
return [
{ id: uid(), type: "sticker", text: "DRAG ME →", ...px(0.30, 0.12), r: -6, bg: "var(--acid)" },
{ id: uid(), type: "emoji", text: "★", ...px(0.62, 0.20), r: 12 },
{ id: uid(), type: "text", text: "open studio.", ...px(0.28, 0.38), r: -3, color: "var(--ink)" },
{ id: uid(), type: "shape", shape: "circle", ...px(0.78, 0.46), r: 0, bg: "var(--pink)", w: 110, h: 110 },
{ id: uid(), type: "sticker", text: "★ ★ ★ ★ ★", ...px(0.50, 0.66), r: 4, bg: "var(--pink)", color: "var(--paper)" },
{ id: uid(), type: "emoji", text: "✦", ...px(0.86, 0.14), r: -10, color: "var(--blue)" },
];
}
function LiveCanvas() {
const [bg, setBg] = lcUseState(LC_BG_OPTIONS[0].val);
const [items, setItems] = lcUseState(() => initialItems());
const [stageSize, setStageSize] = lcUseState({ w: 900, h: 560 });
const stageRef = lcUseRef(null);
const dragRef = lcUseRef(null);
const didInitRef = lcUseRef(false);
// Observe stage size; on first measurement, reflow initial items to fit.
lcUseEffect(() => {
const el = stageRef.current;
if (!el) return;
const measure = () => {
const r = el.getBoundingClientRect();
setStageSize({ w: r.width, h: r.height });
if (!didInitRef.current && r.width > 0) {
setItems(initialItems(r.width, r.height));
didInitRef.current = true;
}
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
window.addEventListener("resize", measure);
return () => {
ro.disconnect();
window.removeEventListener("resize", measure);
};
}, []);
const spawnRange = () => ({
minX: 20, maxX: Math.max(40, stageSize.w - 140),
minY: 20, maxY: Math.max(40, stageSize.h - 100),
});
const addSticker = () => {
const text = LC_STICKERS[Math.floor(Math.random() * LC_STICKERS.length)];
const colors = ["var(--acid)","var(--pink)","var(--blue)","var(--mint)","var(--lilac)","var(--ink)"];
const bg = colors[Math.floor(Math.random() * colors.length)];
const color = (bg === "var(--acid)" || bg === "var(--mint)" || bg === "var(--lilac)") ? "var(--ink)" : "var(--paper)";
spawn({ type: "sticker", text, r: rand(-10, 10), bg, color });
};
const addEmoji = () => spawn({ type: "emoji", text: LC_EMOJIS[Math.floor(Math.random()*LC_EMOJIS.length)], r: rand(-15, 15), color: ["var(--ink)","var(--pink)","var(--blue)","var(--lava)"][Math.floor(Math.random()*4)] });
const addText = () => spawn({ type: "text", text: LC_PHRASES[Math.floor(Math.random()*LC_PHRASES.length)], r: rand(-5, 5), color: "var(--ink)" });
const addShape = () => {
const shape = ["circle","square","triangle"][Math.floor(Math.random()*3)];
spawn({ type: "shape", shape, r: rand(0, 30), bg: LC_SHAPE_COLORS[Math.floor(Math.random()*LC_SHAPE_COLORS.length)], w: 110, h: 110 });
};
const clearAll = () => setItems([]);
const reset = () => setItems(initialItems(stageSize.w, stageSize.h));
const shuffle = () => {
const range = spawnRange();
setItems(items => items.map(it => ({
...it,
r: rand(-12, 12),
x: rand(range.minX, range.maxX),
y: rand(range.minY, range.maxY),
})));
};
function rand(a, b) { return Math.round(a + Math.random() * (b - a)); }
function spawn(partial) {
const r = spawnRange();
setItems(items => [...items, { id: uid(), x: rand(r.minX, r.maxX), y: rand(r.minY, r.maxY), ...partial }]);
}
function startDrag(e, id) {
const item = items.find(i => i.id === id);
if (!item) return;
const stageRect = stageRef.current.getBoundingClientRect();
const point = e.touches ? e.touches[0] : e;
dragRef.current = {
id,
offX: point.clientX - stageRect.left - item.x,
offY: point.clientY - stageRect.top - item.y,
moved: false,
};
window.addEventListener("mousemove", onDrag);
window.addEventListener("mouseup", endDrag);
window.addEventListener("touchmove", onDrag, { passive: false });
window.addEventListener("touchend", endDrag);
e.preventDefault();
}
function onDrag(e) {
if (!dragRef.current) return;
const stageRect = stageRef.current.getBoundingClientRect();
const point = e.touches ? e.touches[0] : e;
const nx = point.clientX - stageRect.left - dragRef.current.offX;
const ny = point.clientY - stageRect.top - dragRef.current.offY;
dragRef.current.moved = true;
setItems(items => items.map(it => it.id === dragRef.current.id ? { ...it, x: nx, y: ny } : it));
if (e.preventDefault) e.preventDefault();
}
function endDrag() {
window.removeEventListener("mousemove", onDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchmove", onDrag);
window.removeEventListener("touchend", endDrag);
setTimeout(() => { dragRef.current = null; }, 50);
}
function rotateItem(id) {
if (dragRef.current && dragRef.current.moved) return;
setItems(items => items.map(it => it.id === id ? { ...it, r: (it.r || 0) + 18 } : it));
}
function deleteItem(e, id) {
e.preventDefault();
setItems(items => items.filter(it => it.id !== id));
}
return (
Most agency websites show you a portfolio. We're handing you the tools.
Spawn stickers, drag emoji, throw shapes around. The composition you make is yours.
The studio
is open.
↳ click = rotate · drag = move · right-click = delete
Tools
Background
Stage