🎵 Ringtone
Composer
Compose retro monophonic ringtones — play and export WAV.
⬇ Install app
EN
ΕΛ
▷ Demo
// ===== MEASURE (init/resize μόνο — όχι ανά keystroke) ===== function measure() { var probe = document.createElement("span"); probe.style.visibility = "hidden"; probe.style.whiteSpace = "pre"; probe.style.font = "inherit"; probe.textContent = "0000000000"; paper.appendChild(probe); var w10 = probe.getBoundingClientRect().width; probe.remove(); if (w10 > 0) charsPerLine = Math.max(20, Math.floor((paper.clientWidth - 88) / (w10 / 10))); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function makeLine(i) { var div = document.createElement("div"); div.className = "line"; if (i === curLine) { div.setAttribute("data-cur", "1"); div.innerHTML = escapeHtml(lines[i]) + '
'; } else { div.innerHTML = escapeHtml(lines[i]) || " "; } return div; } // ===== RENDER (clean: sync children, ενημέρωση μόνο της τρέχουσας γραμμής) ===== function render(full) { if (full) { // Πλήρης επανακατασκευή: αλλαγή δομής (νέα γραμμή, backspace στην αρχή, Esc, resize) paper.innerHTML = ""; for (var i = 0; i < lines.length; i++) { var div = makeLine(i); if (i === curLine) renderLine = div; paper.appendChild(div); } } else { // Ταχεία διαδρομή: μόνο η τρέχουσα γραμμή ξαναγράφεται renderLine.innerHTML = escapeHtml(lines[curLine]) + '
'; } renderLine.id = "renderLine"; renderCounters(); // Scroll: κράτα την τρέχουσα γραμμή ορατή var cur = paper.querySelector('.line[data-cur="1"]'); if (cur) { var offset = cur.offsetTop - 40; paperInner.style.top = Math.max(0, offset - 60) * -1 + "px"; } // Κουδούνι πλησίον άκρης γραμμής: 12 γράμματα πριν το τέλος (>=, flag ανά γραμμή) if (lines[curLine].length >= charsPerLine - 12 && !lineBelled) { lineBelled = true; if (soundChk.checked) bellSound(); } } function renderCounters() { var all = lines.join("\n"); var wordCount = all.trim() ? all.trim().split(/\s+/).length : 0; var charCount = all.replace(/\n/g, "").length; keysHint.textContent = wordCount + " " + T.words[currentLang] + " · " + charCount + " " + T.chars[currentLang]; } // ===== INPUT ===== function typeChar(ch) { if (lines[curLine].length >= charsPerLine) { newLine(); } else { lineBelled = false; } // «Κολλημένα πλήκτρα»: 4% — παροδική οριζόντια μετατόπιση του χαρτιού if (misalignChk.checked && Math.random() < 0.04) { paper.classList.remove("stuck"); void paper.offsetWidth; paper.classList.add("stuck"); } lines[curLine] += ch; render(false); paper.classList.remove("keyed"); void paper.offsetWidth; paper.classList.add("keyed"); if (soundChk.checked) { if (ch === " ") spaceSound(); else keySound(); } } function newLine() { lines.push(""); curLine++; lineBelled = false; render(true); if (soundChk.checked) returnSound(); } function backspace() { if (lines[curLine].length > 0) { lines[curLine] = lines[curLine].slice(0, -1); lineBelled = false; } else if (curLine > 0) { lines.pop(); curLine--; } else { return; } render(true); if (soundChk.checked) keySound(); // «ξε-πάτημα» — μικρό κλικ } function newSheet() { lines = [""]; curLine = 0; lineBelled = false; render(true); if (soundChk.checked) sheetSound(); } // ===== KEYBOARD (desktop) ===== document.addEventListener("keydown", function (e) { // Μηδέν παρεμβολή σε πραγματικά controls (και κουμπιά — το Space κάνει κλικ εκεί) var tag = (e.target && e.target.tagName || "").toLowerCase(); if (tag === "input" || tag === "select" || tag === "textarea" || tag === "button") return; if (e.key === "Escape") { newSheet(); return; } if (e.key === "Enter") { e.preventDefault(); newLine(); return; } if (e.key === "Backspace") { e.preventDefault(); backspace(); return; } // Εκτυπώσιμοι χαρακτήρες μόνο if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); typeChar(e.key); } }); // ===== MOBILE: κρυφό textarea για εγγενές πληκτρολόγιο ===== var bridge = document.createElement("textarea"); bridge.setAttribute("aria-hidden", "true"); bridge.style.position = "fixed"; bridge.style.opacity = "0"; bridge.style.pointerEvents = "none"; bridge.style.left = "-9999px"; document.body.appendChild(bridge); paper.addEventListener("click", function () { bridge.focus({ preventScroll: true }); }); bridge.addEventListener("input", function () { var v = bridge.value; if (v) { for (var i = 0; i < v.length; i++) typeChar(v[i]); bridge.value = ""; } }); bridge.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); newLine(); } else if (e.key === "Backspace") { e.preventDefault(); backspace(); } }); // ===== BUTTONS ===== bellBtn.addEventListener("click", function () { if (soundChk.checked) bellSound(); }); returnBtn.addEventListener("click", function () { newLine(); }); clearBtn.addEventListener("click", function () { newSheet(); }); // Πρώτο user gesture ενεργοποιεί το AudioContext document.addEventListener("pointerdown", function once() { ac(); document.removeEventListener("pointerdown", once); }, { once: true }); window.addEventListener("resize", function () { measure(); render(true); }); // ===== INIT ===== applyStatic(); measure(); render(true); // ===== ΕΓΚΑΤΑΣΤΑΣΗ (PWA) ===== var deferredPrompt = null; window.addEventListener("beforeinstallprompt", function (e) { e.preventDefault(); deferredPrompt = e; installBtn.hidden = false; }); installBtn.addEventListener("click", function () { if (!deferredPrompt) return; deferredPrompt.prompt(); deferredPrompt = null; installBtn.hidden = true; }); window.addEventListener("appinstalled", function () { deferredPrompt = null; installBtn.hidden = true; }); // ===== SERVICE WORKER (offline — κοινός SW της σουίτας στη ρίζα) ===== if ("serviceWorker" in navigator) { window.addEventListener("load", function () { navigator.serviceWorker.register("../sw.js").catch(function (err) { console.warn("SW registration failed:", err); }); }); } })();