diff --git a/LetterComposer copy.jsx b/LetterComposer copy.jsx
deleted file mode 100644
index 71c44a5..0000000
--- a/LetterComposer copy.jsx
+++ /dev/null
@@ -1,328 +0,0 @@
-import React, { useRef, useState, useEffect } from "react";
-
-const KASZTA_WIDTH = 1618;
-const KASZTA_HEIGHT = 1080;
-const SLOTS_COUNT = 20;
-
-function getImageWidth(src) {
- return new Promise((resolve) => {
- const img = new window.Image();
- img.onload = () => resolve(img.width);
- img.src = src;
- });
-}
-
-export default function LetterComposer() {
- const [letterFields, setLetterFields] = useState([]);
- const [slots, setSlots] = useState(Array(SLOTS_COUNT).fill(null));
- const [activeLetter, setActiveLetter] = useState(null);
- const [ghostPos, setGhostPos] = useState({ x: 0, y: 0, visible: false });
- const [isDragging, setIsDragging] = useState(false);
- const kasztaRef = useRef();
- const wierszownikRef = useRef();
- const [kasztaW, setKasztaW] = useState(KASZTA_WIDTH);
-
- // BLOKUJ SCROLL strony
- useEffect(() => {
- const oldOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
- return () => { document.body.style.overflow = oldOverflow; };
- }, []);
-
- // Responsywna szerokość kaszty
- useEffect(() => {
- function handleResize() {
- if (kasztaRef.current) {
- const parentW = kasztaRef.current.parentElement.offsetWidth;
- setKasztaW(Math.min(parentW, KASZTA_WIDTH));
- }
- }
- handleResize();
- window.addEventListener("resize", handleResize);
- return () => window.removeEventListener("resize", handleResize);
- }, []);
-
- useEffect(() => {
- fetch('/poz.json')
- .then(res => res.json())
- .then(setLetterFields)
- .catch(() => setLetterFields([]));
- }, []);
-
- // DRAG START (mouse/touch na field)
- const handleFieldDragStart = async (field, e) => {
- e.preventDefault();
- const width = await getImageWidth(field.img);
- setActiveLetter({ ...field, width });
- let x = 0, y = 0;
- if (e.touches && e.touches[0]) {
- x = e.touches[0].clientX;
- y = e.touches[0].clientY;
- } else if (e.clientX && e.clientY) {
- x = e.clientX;
- y = e.clientY;
- }
- setGhostPos({ x, y, visible: true });
- setIsDragging(true);
- };
-
- // DRAG MOVE & DROP (document-level, działa na iOS)
- useEffect(() => {
- if (!isDragging) return;
- const moveGhost = (e) => {
- setGhostPos({
- x: e.clientX,
- y: e.clientY,
- visible: true
- });
- };
- const moveGhostTouch = (e) => {
- if (e.touches && e.touches[0]) {
- setGhostPos({
- x: e.touches[0].clientX,
- y: e.touches[0].clientY,
- visible: true
- });
- }
- };
- const handleDrop = (e) => {
- let x = 0, y = 0;
- if (e.changedTouches && e.changedTouches[0]) {
- x = e.changedTouches[0].clientX;
- y = e.changedTouches[0].clientY;
- } else if (e.clientX && e.clientY) {
- x = e.clientX;
- y = e.clientY;
- }
- if (wierszownikRef.current) {
- const rect = wierszownikRef.current.getBoundingClientRect();
- if (
- x >= rect.left &&
- x <= rect.right &&
- y >= rect.top &&
- y <= rect.bottom
- ) {
- placeLetter();
- } else {
- setActiveLetter(null);
- setGhostPos({ x: 0, y: 0, visible: false });
- }
- }
- setIsDragging(false);
- };
- document.addEventListener("mousemove", moveGhost);
- document.addEventListener("touchmove", moveGhostTouch, { passive: false });
- document.addEventListener("mouseup", handleDrop);
- document.addEventListener("touchend", handleDrop, { passive: false });
- return () => {
- document.removeEventListener("mousemove", moveGhost);
- document.removeEventListener("touchmove", moveGhostTouch);
- document.removeEventListener("mouseup", handleDrop);
- document.removeEventListener("touchend", handleDrop);
- };
- // eslint-disable-next-line
- }, [isDragging]);
-
- // Umieszcza literę na wierszowniku
- function placeLetter() {
- let idx = slots.lastIndexOf(null);
- if (idx === -1) idx = 0;
- const updatedSlots = [...slots];
- updatedSlots[idx] = { ...activeLetter, id: Math.random().toString(36) };
- setSlots(updatedSlots);
- setActiveLetter(null);
- setGhostPos({ x: 0, y: 0, visible: false });
- setIsDragging(false);
- }
-
- // Kaszta – klik/tap poza polem kaszty anuluje ghosta
- function handleKasztaBackgroundClick(e) {
- setActiveLetter(null);
- setGhostPos({ x: 0, y: 0, visible: false });
- setIsDragging(false);
- }
-
- // Usuwanie liter z wierszownika
- const removeLetterFromSlot = (i) => {
- const updatedSlots = [...slots];
- updatedSlots[i] = null;
- setSlots(updatedSlots);
- };
-
- const scale = kasztaW / KASZTA_WIDTH;
- const kasztaH = kasztaW * (KASZTA_HEIGHT / KASZTA_WIDTH);
- const lineW = kasztaW * 0.8; // WIERSZOWNIK 80% kaszty
-
- function renderLettersOnLine() {
- let right = 0;
- let visibleSlots = [];
- for (let i = slots.length - 1; i >= 0; i--) {
- const slot = slots[i];
- if (!slot) continue;
- right += slot.width * scale;
- visibleSlots.push(
-
removeLetterFromSlot(i)}
- title="Kliknij, aby usunąć literę z wierszownika"
- >
-

-
- );
- }
- return visibleSlots.reverse();
- }
-
- // Ghost litera
- const renderGhostLetter = () => {
- if (!activeLetter || !ghostPos.visible) return null;
- return (
-
- );
- };
-
- return (
-
- {/* Kaszta */}
-
- {/* Tło łapiące kliknięcie na kaszcie */}
-
-

- {/* Fieldy na kaszcie – tylko drag start */}
- {letterFields.map(field => (
-
-
- {/* Wierszownik */}
-
-
-
- {renderLettersOnLine()}
-
-
- Educational Game for the Muzeum Książki Artystycznej w Łodzi by
peterwolf.pl
-
- {renderGhostLetter()}
-
-
-
-
- );
-}
diff --git a/LetterComposer.jsx b/LetterComposer.jsx
index b569a9c..8c3ee89 100644
--- a/LetterComposer.jsx
+++ b/LetterComposer.jsx
@@ -3,6 +3,10 @@ import React, { useRef, useState, useEffect } from "react";
const KASZTA_WIDTH = 1618;
const KASZTA_HEIGHT = 1080;
const SLOTS_COUNT = 20;
+const LETTER_HEIGHT = 96;
+const LINE_OFFSET_RIGHT = 340;
+const LINE_OFFSET_BOTTOM = 240;
+const WIERSZOWNIK_SRC = "/assets/wierszownik.jpg";
function getImageWidth(src) {
return new Promise((resolve) => {
@@ -18,9 +22,17 @@ export default function LetterComposer({ onMoveLineToPage }) {
const [activeLetter, setActiveLetter] = useState(null);
const [ghostPos, setGhostPos] = useState({ x: 0, y: 0, visible: false });
const [isDragging, setIsDragging] = useState(false);
+ const [pickupAnim, setPickupAnim] = useState(false);
const kasztaRef = useRef();
const wierszownikRef = useRef();
const [kasztaW, setKasztaW] = useState(KASZTA_WIDTH);
+ const [wierszownikDims, setWierszownikDims] = useState({ width: 1, height: 1 });
+
+ useEffect(() => {
+ const img = new window.Image();
+ img.onload = () => setWierszownikDims({ width: img.width, height: img.height });
+ img.src = WIERSZOWNIK_SRC;
+ }, []);
// BLOKUJ SCROLL strony
useEffect(() => {
@@ -58,6 +70,8 @@ export default function LetterComposer({ onMoveLineToPage }) {
e.preventDefault();
const width = await getImageWidth(field.img);
setActiveLetter({ ...field, width });
+ setPickupAnim(true);
+ setTimeout(() => setPickupAnim(false), 300);
let x = 0, y = 0;
if (e.touches && e.touches[0]) {
x = e.touches[0].clientX;
@@ -153,9 +167,15 @@ export default function LetterComposer({ onMoveLineToPage }) {
setSlots(updatedSlots);
};
- const scale = kasztaW / KASZTA_WIDTH;
+
+ const kasztaScale = kasztaW / KASZTA_WIDTH;
const kasztaH = kasztaW * (KASZTA_HEIGHT / KASZTA_WIDTH);
const lineW = kasztaW * 0.8; // WIERSZOWNIK 80% kaszty
+ const wierszScale = lineW / wierszownikDims.width;
+ const lineH = wierszownikDims.height * wierszScale;
+ const letterScale = wierszScale * 2;
+ const offsetRight = LINE_OFFSET_RIGHT * wierszScale;
+ const offsetTop = lineH - LINE_OFFSET_BOTTOM * wierszScale - LETTER_HEIGHT * letterScale;
function renderLettersOnLine() {
let right = 0;
@@ -163,16 +183,16 @@ export default function LetterComposer({ onMoveLineToPage }) {
for (let i = slots.length - 1; i >= 0; i--) {
const slot = slots[i];
if (!slot) continue;
- right += slot.width * scale;
+ right += slot.width * letterScale;
visibleSlots.push(
@@ -202,14 +222,15 @@ export default function LetterComposer({ onMoveLineToPage }) {
alt={activeLetter.char}
style={{
position: "fixed",
- left: ghostPos.x - (activeLetter.width * scale) / 2,
- top: ghostPos.y - (96 * scale),
- width: activeLetter.width * scale,
- height: 96 * scale,
+ left: ghostPos.x - (activeLetter.width * letterScale) / 2,
+ top: ghostPos.y - (LETTER_HEIGHT * letterScale) / 2,
+ width: activeLetter.width * letterScale,
+ height: LETTER_HEIGHT * letterScale,
pointerEvents: "none",
zIndex: 1000,
opacity: 1,
- filter: "drop-shadow(2px 2px 2px #999)"
+ filter: "drop-shadow(2px 2px 2px #999)",
+ animation: pickupAnim ? "letter-pop 0.3s ease-out forwards" : undefined
}}
/>
);
@@ -224,7 +245,6 @@ export default function LetterComposer({ onMoveLineToPage }) {
minHeight: "100vh",
display: "flex",
flexDirection: "column",
- background: "#f5f6f8",
alignItems: "center",
justifyContent: "stretch",
overflow: "hidden",
@@ -289,10 +309,10 @@ export default function LetterComposer({ onMoveLineToPage }) {
aria-label="Wybierz czcionkę"
style={{
position: "absolute",
- left: Math.min(field.x1, field.x2) * scale,
- top: Math.min(field.y1, field.y2) * scale,
- width: Math.abs(field.x2 - field.x1) * scale,
- height: Math.abs(field.y2 - field.y1) * scale,
+ left: Math.min(field.x1, field.x2) * kasztaScale,
+ top: Math.min(field.y1, field.y2) * kasztaScale,
+ width: Math.abs(field.x2 - field.x1) * kasztaScale,
+ height: Math.abs(field.y2 - field.y1) * kasztaScale,
border: "0px solid #2563eb",
background: "rgba(96,165,250,0.0)",
borderRadius: "10px",
@@ -319,37 +339,21 @@ export default function LetterComposer({ onMoveLineToPage }) {
style={{
position: "relative",
width: lineW,
- minHeight: 116 * scale,
+ height: lineH,
margin: "1px auto 0px auto",
- borderRadius: 8 * scale,
- background: "#a6a3a8",
touchAction: "none",
flexShrink: 0,
boxSizing: "border-box"
}}
>
-
-
{renderLettersOnLine()}
diff --git a/PageComposer.jsx b/PageComposer.jsx
index 3c267e9..3bcabe1 100644
--- a/PageComposer.jsx
+++ b/PageComposer.jsx
@@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState } from "react";
const A4_WIDTH = 796;
const A4_HEIGHT = 1123;
+const SHEET_OFFSET_RIGHT = 120;
+const SHEET_OFFSET_TOP = 170;
+const LETTER_SCALE = 2;
+const LETTER_BASE_HEIGHT = 96 / 3;
@@ -17,22 +21,33 @@ export default function PageComposer({
}) {
const [pageW, setPageW] = useState(A4_WIDTH);
const wrapperRef = useRef();
+ const [sheetDims, setSheetDims] = useState({ width: A4_WIDTH, height: A4_HEIGHT });
+
+ useEffect(() => {
+ const img = new Image();
+ img.src = "/assets/blacha.png";
+ img.onload = () => {
+ setSheetDims({ width: img.width, height: img.height });
+ };
+ }, []);
useEffect(() => {
function handleResize() {
const maxW = window.innerWidth * 0.95;
const stopkaH = 40 + 18;
const maxH = window.innerHeight - stopkaH - 32;
- const byHeight = maxH * (A4_WIDTH / A4_HEIGHT);
- setPageW(Math.min(A4_WIDTH, maxW, byHeight));
+ const byHeight = maxH * (sheetDims.width / sheetDims.height);
+ setPageW(Math.min(sheetDims.width, maxW, byHeight));
}
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
- }, []);
+ }, [sheetDims]);
- const scale = pageW / A4_WIDTH;
- const pageH = pageW * (A4_HEIGHT / A4_WIDTH);
+ const scale = pageW / sheetDims.width;
+ const pageH = pageW * (sheetDims.height / sheetDims.width);
+ const sheetOffsetRight = SHEET_OFFSET_RIGHT * scale;
+ const sheetOffsetTop = SHEET_OFFSET_TOP * scale;
// DRAG
const [dragIndex, setDragIndex] = useState(null);
@@ -173,7 +188,7 @@ export default function PageComposer({
return {
background: "#e3f2ff",
borderRadius: 4 * scale,
- minHeight: 32 * scale,
+ minHeight: LETTER_BASE_HEIGHT * LETTER_SCALE * scale,
outline: "2px dashed #28b0ef",
transition: "background 0.12s",
};
@@ -205,7 +220,6 @@ export default function PageComposer({
style={{
minHeight: "100vh",
width: "100vw",
- background: "#f5f6f8",
display: "flex",
flexDirection: "column",
alignItems: "center",
@@ -230,8 +244,10 @@ export default function PageComposer({
{lines.map((line, i) => {
@@ -257,9 +276,9 @@ export default function PageComposer({
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "flex-end",
- margin: `${30 * scale}px ${20 * scale}px ${-24 * scale}px 0`,
- minHeight: 96 / 3 * scale,
- maxWidth: `calc(100% - ${40 * scale}px)`,
+ margin: `0 0 ${8 * scale}px 0`,
+ minHeight: LETTER_BASE_HEIGHT * LETTER_SCALE * scale,
+ maxWidth: `calc(100% - ${sheetOffsetRight}px)`,
cursor: dragIndex === null ? "grab" : "default",
userSelect: "none",
touchAction: "none",
@@ -273,8 +292,8 @@ export default function PageComposer({
key={j}
src={letter.img}
alt={letter.char}
- width={(letter.width / 3) * scale}
- height={(96 / 3) * scale}
+ width={(letter.width / 3) * scale * LETTER_SCALE}
+ height={LETTER_BASE_HEIGHT * scale * LETTER_SCALE}
style={{ marginLeft: 0, pointerEvents: "none" }}
draggable={false}
/>
@@ -287,8 +306,8 @@ export default function PageComposer({
diff --git a/PrintModule.jsx b/PrintModule.jsx
index e7bfe40..e3b60ee 100644
--- a/PrintModule.jsx
+++ b/PrintModule.jsx
@@ -5,6 +5,7 @@ const A4_HEIGHT = 1123;
export default function PrintModule({ lines, onBack }) {
const [pageW, setPageW] = useState(A4_WIDTH);
+ const [animReady, setAnimReady] = useState(false);
// Dynamiczne skalowanie dwóch kartek w oknie
useEffect(() => {
@@ -25,6 +26,11 @@ export default function PrintModule({ lines, onBack }) {
const scale = pageW / A4_WIDTH;
const pageH = pageW * (A4_HEIGHT / A4_WIDTH);
+ useEffect(() => {
+ const t = setTimeout(() => setAnimReady(true), 500);
+ return () => clearTimeout(t);
+ }, []);
+
// Lustrzane odbicie: linie od dołu, każda linia od końca i flipped poziomo
const mirroredLines = [...lines];
@@ -33,7 +39,6 @@ export default function PrintModule({ lines, onBack }) {
style={{
minHeight: "100vh",
width: "100vw",
- background: "#f5f6f8",
display: "flex",
flexDirection: "column",
alignItems: "center",
@@ -123,15 +128,23 @@ export default function PrintModule({ lines, onBack }) {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
- justifyContent: "flex-start"
+ justifyContent: "flex-start",
+ transform: animReady
+ ? "translateX(0) rotateX(0deg)"
+ : `translateX(-${pageW + 48 * scale}px) rotateX(180deg)`,
+ "--dx": `${pageW + 48 * scale}px`,
+ animation: animReady
+ ? "right-page-flip 1s ease forwards"
+ : "none",
+ transformStyle: "preserve-3d",
+ backfaceVisibility: "hidden"
}}
>
-
-
Vite + React
-
-
-
- Edit src/App.jsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
-}
-
-export default App
diff --git a/assets/.gitkeep b/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/index.css b/index.css
index b1fe651..64dc43e 100644
--- a/index.css
+++ b/index.css
@@ -1,10 +1,10 @@
-:root {
- @font-face {font-family: GrohmanGrotesk-Classic;
+@font-face {
+ font-family: GrohmanGrotesk-Classic;
src: url('fonts/GG-Classic_2.0.woff') format('woff'),
url('fonts/GG-Classic_2.0.woff2') format('woff2');
- }}
+}
- :root {
+:root {
font-family: GrohmanGrotesk-Classic;
line-height: 1.5;
font-weight: 400;
@@ -12,9 +12,13 @@
-webkit-user-select: none;
-ms-user-select: none;
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
+ color-scheme: light;
+ color: #213547;
+ background-color: #d4d4d4;
+ background-image: url('/assets/bg.png');
+ background-repeat: repeat;
+ background-size: var(--bg-tile-size);
+ --bg-tile-size: 64px;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -37,6 +41,10 @@ body {
place-items: center;
min-width: 320px;
min-height: 100vh;
+ background-color: #d4d4d4;
+ background-image: url('/assets/bg.png');
+ background-repeat: repeat;
+ background-size: var(--bg-tile-size);
}
h1 {
@@ -66,7 +74,6 @@ button:focus-visible {
@media (prefers-color-scheme: light) {
:root {
color: #213547;
- background-color: #ffffff;
}
a:hover {
color: #747bff;
@@ -75,3 +82,24 @@ button:focus-visible {
background-color: #f9f9f9;
}
}
+
+@keyframes letter-pop {
+ from {
+ transform: scale(0);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+
+@keyframes right-page-flip {
+ 0% {
+ transform: translateX(calc(-1 * var(--dx))) rotateX(180deg) translateY(0);
+ }
+ 50% {
+ transform: translateX(calc(-1 * var(--dx))) rotateX(180deg) translateY(-40px);
+ }
+ 100% {
+ transform: translateX(0) rotateX(0deg) translateY(0);
+ }
+}