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" - > - {slot.char} -
- ); - } - return visibleSlots.reverse(); - } - - // Ghost litera - const renderGhostLetter = () => { - if (!activeLetter || !ghostPos.visible) return null; - return ( - {activeLetter.char} - ); - }; - - return ( -
- {/* Kaszta */} -
- {/* Tło łapiące kliknięcie na kaszcie */} -
- Kaszta zecerska - {/* 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); + } +}