Created
May 20, 2026 16:53
-
-
Save hcivelek/abd751239d04105a211e3d3d04630fbf to your computer and use it in GitHub Desktop.
Word Writing Examples
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="tr"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Kelime Silüeti Oluşturucu</title> | |
| <style> | |
| :root { | |
| --ink: #111827; | |
| --muted: #6b7280; | |
| --line: #d1d5db; | |
| --paper: #ffffff; | |
| --bg: #f3f4f6; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| background: var(--bg); | |
| color: var(--ink); | |
| } | |
| .app { | |
| display: grid; | |
| grid-template-columns: 360px 1fr; | |
| min-height: 100vh; | |
| } | |
| .panel { | |
| padding: 20px; | |
| background: #fff; | |
| border-right: 1px solid #e5e7eb; | |
| position: sticky; | |
| top: 0; | |
| height: 100vh; | |
| overflow: auto; | |
| } | |
| .panel h1 { | |
| margin: 0 0 8px; | |
| font-size: 22px; | |
| } | |
| .panel p { | |
| margin: 0 0 18px; | |
| color: var(--muted); | |
| line-height: 1.35; | |
| font-size: 14px; | |
| } | |
| label { | |
| display: block; | |
| margin: 14px 0 6px; | |
| font-weight: 650; | |
| font-size: 13px; | |
| } | |
| textarea, input, select { | |
| width: 100%; | |
| border: 1px solid #d1d5db; | |
| border-radius: 10px; | |
| padding: 10px 12px; | |
| font: inherit; | |
| background: #fff; | |
| } | |
| textarea { | |
| min-height: 180px; | |
| resize: vertical; | |
| line-height: 1.45; | |
| } | |
| .row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| } | |
| .check { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 12px; | |
| color: #374151; | |
| font-size: 14px; | |
| } | |
| .check input { width: auto; } | |
| button { | |
| width: 100%; | |
| margin-top: 18px; | |
| border: 0; | |
| border-radius: 12px; | |
| padding: 12px 14px; | |
| background: #111827; | |
| color: #fff; | |
| font-weight: 700; | |
| cursor: pointer; | |
| } | |
| button.secondary { | |
| background: #e5e7eb; | |
| color: #111827; | |
| margin-top: 8px; | |
| } | |
| .preview-wrap { | |
| padding: 28px; | |
| overflow: auto; | |
| } | |
| .paper { | |
| width: 210mm; | |
| min-height: 297mm; | |
| margin: 0 auto; | |
| background: var(--paper); | |
| box-shadow: 0 12px 30px rgba(0,0,0,.12); | |
| padding: 16mm 14mm; | |
| } | |
| .worksheet-title { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 12px; | |
| align-items: end; | |
| margin-bottom: 8mm; | |
| font-size: 14px; | |
| } | |
| .name-line { | |
| min-width: 62mm; | |
| border-bottom: 1px solid #9ca3af; | |
| height: 20px; | |
| } | |
| .item { | |
| margin: 0 0 11mm; | |
| break-inside: avoid; | |
| } | |
| .word-label { | |
| font-size: 22px; | |
| letter-spacing: .02em; | |
| margin-bottom: 2mm; | |
| color: #111827; | |
| } | |
| svg { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| overflow: visible; | |
| } | |
| .guide { stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 4; } | |
| .baseline { stroke: #9ca3af; stroke-width: 1.2; } | |
| .silhouette { fill: #ffffff; stroke: #111827; stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; } | |
| .letter-cut { stroke: #e5e7eb; stroke-width: 1; } | |
| .faint-text { fill: #9ca3af; font-size: 14px; } | |
| @media print { | |
| body { background: #fff; } | |
| .panel { display: none; } | |
| .app { display: block; } | |
| .preview-wrap { padding: 0; } | |
| .paper { | |
| width: auto; | |
| min-height: auto; | |
| margin: 0; | |
| box-shadow: none; | |
| padding: 12mm 12mm; | |
| } | |
| @page { size: A4 portrait; margin: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <aside class="panel"> | |
| <h1>Kelime silüeti</h1> | |
| <p>Her satıra bir kelime yaz. Uygulama harflerin yukarı çıkan ve aşağı sarkan kısımlarına göre yazı kutusu/silüeti oluşturur.</p> | |
| <label for="words">Kelimeler</label> | |
| <textarea id="words">sandalye | |
| kalem | |
| okul | |
| ağaç | |
| yaprak | |
| kitap | |
| fil | |
| çiçek</textarea> | |
| <div class="row"> | |
| <div> | |
| <label for="cellWidth">Harf genişliği</label> | |
| <input id="cellWidth" type="number" value="28" min="16" max="60" /> | |
| </div> | |
| <div> | |
| <label for="heightScale">Yükseklik</label> | |
| <input id="heightScale" type="number" value="1" min="0.7" max="1.6" step="0.1" /> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div> | |
| <label for="copies">Tekrar sayısı</label> | |
| <input id="copies" type="number" value="2" min="1" max="6" /> | |
| </div> | |
| <div> | |
| <label for="labelMode">Kelime yazısı</label> | |
| <select id="labelMode"> | |
| <option value="left">Solda göster</option> | |
| <option value="none">Gizle</option> | |
| </select> | |
| </div> | |
| </div> | |
| <label class="check"> | |
| <input id="showGuides" type="checkbox" checked /> | |
| Kılavuz çizgileri göster | |
| </label> | |
| <label class="check"> | |
| <input id="showLetterCuts" type="checkbox" /> | |
| Harf ayrımlarını hafif göster | |
| </label> | |
| <button onclick="render()">Oluştur</button> | |
| <button class="secondary" onclick="window.print()">Yazdır / PDF al</button> | |
| </aside> | |
| <main class="preview-wrap"> | |
| <section id="paper" class="paper"></section> | |
| </main> | |
| </div> | |
| <script> | |
| // El yazısı/pratik öğretim için basit sınıflandırma. | |
| // İsterseniz bu listeleri çocuğun öğretmeninin kullandığı harf formuna göre değiştirin. | |
| const ASCENDERS = new Set(["b", "d", "f", "h", "k", "l", "t"]); | |
| const DESCENDERS = new Set(["g", "ğ", "j", "p", "q", "y"]); | |
| const BOTH = new Set([]); // örn. f aşağı da insin istenirse: new Set(["f"]) | |
| const TURKISH_LOWER_MAP = { | |
| "I": "ı", | |
| "İ": "i" | |
| }; | |
| function trLower(char) { | |
| return TURKISH_LOWER_MAP[char] || char.toLocaleLowerCase("tr-TR"); | |
| } | |
| function letterProfile(char) { | |
| const c = trLower(char); | |
| if (BOTH.has(c)) return "both"; | |
| if (ASCENDERS.has(c)) return "asc"; | |
| if (DESCENDERS.has(c)) return "desc"; | |
| return "mid"; | |
| } | |
| function xAdvance(char, cellWidth) { | |
| const c = trLower(char); | |
| if (c === "i" || c === "ı" || c === "l" || c === "j") return cellWidth * 0.62; | |
| if (c === "m" || c === "w") return cellWidth * 1.28; | |
| if (c === " " || c === "-") return cellWidth * 0.55; | |
| return cellWidth; | |
| } | |
| function rectForProfile(profile, x, w, yBase, xHeight, asc, desc) { | |
| if (profile === "asc") return { x, y: yBase - asc, w, h: asc }; | |
| if (profile === "desc") return { x, y: yBase - xHeight, w, h: xHeight + desc }; | |
| if (profile === "both") return { x, y: yBase - asc, w, h: asc + desc }; | |
| return { x, y: yBase - xHeight, w, h: xHeight }; | |
| } | |
| // Komşu harf kutularının birleşiminden dış kontur üretir. | |
| // Grid tabanlı çalıştığı için sandalye/kalem gibi kelimelerde basamaklı silüet verir. | |
| function buildOutlinePath(rects) { | |
| if (!rects.length) return ""; | |
| const items = rects.map(r => ({ | |
| left: r.x, | |
| right: r.x + r.w, | |
| top: r.y, | |
| bottom: r.y + r.h | |
| })); | |
| let d = `M ${items[0].left} ${items[0].top}`; | |
| // Üst kontur: soldan sağa. Harfler arasında sadece dış basamakları takip eder. | |
| for (let i = 0; i < items.length; i++) { | |
| const cur = items[i]; | |
| d += ` L ${cur.right} ${cur.top}`; | |
| const next = items[i + 1]; | |
| if (next && next.top !== cur.top) { | |
| d += ` L ${cur.right} ${next.top}`; | |
| } | |
| } | |
| // En sağ kenar. | |
| const last = items[items.length - 1]; | |
| d += ` L ${last.right} ${last.bottom}`; | |
| // Alt kontur: sağdan sola. Böylece şekil tek parça ve içi boş/beyaz kalır. | |
| for (let i = items.length - 1; i >= 0; i--) { | |
| const cur = items[i]; | |
| d += ` L ${cur.left} ${cur.bottom}`; | |
| const prev = items[i - 1]; | |
| if (prev && prev.bottom !== cur.bottom) { | |
| d += ` L ${cur.left} ${prev.bottom}`; | |
| } | |
| } | |
| d += " Z"; | |
| return d; | |
| } | |
| function makeWordSvg(word, options) { | |
| const scale = options.heightScale; | |
| const cellWidth = options.cellWidth; | |
| const xHeight = 34 * scale; | |
| const asc = 56 * scale; | |
| const desc = 26 * scale; | |
| const topPad = 10; | |
| const leftPad = 10; | |
| const rightPad = 10; | |
| const bottomPad = 10; | |
| const yBase = topPad + asc; | |
| let x = leftPad; | |
| const rects = []; | |
| const cuts = []; | |
| Array.from(word).forEach(char => { | |
| const w = xAdvance(char, cellWidth); | |
| if (char !== " " && char !== "-") { | |
| const profile = letterProfile(char); | |
| rects.push(rectForProfile(profile, x, w, yBase, xHeight, asc, desc)); | |
| cuts.push(x); | |
| } | |
| x += w; | |
| }); | |
| const width = Math.max(220, x + rightPad); | |
| const height = topPad + asc + desc + bottomPad; | |
| const outline = buildOutlinePath(rects); | |
| const guideLines = options.showGuides ? ` | |
| <line class="guide" x1="0" y1="${topPad}" x2="${width}" y2="${topPad}" /> | |
| <line class="guide" x1="0" y1="${yBase - xHeight}" x2="${width}" y2="${yBase - xHeight}" /> | |
| <line class="baseline" x1="0" y1="${yBase}" x2="${width}" y2="${yBase}" /> | |
| <line class="guide" x1="0" y1="${yBase + desc}" x2="${width}" y2="${yBase + desc}" /> | |
| ` : ""; | |
| const cutLines = options.showLetterCuts ? cuts.map(cx => | |
| `<line class="letter-cut" x1="${cx}" y1="${topPad}" x2="${cx}" y2="${yBase + desc}" />` | |
| ).join("\n") : ""; | |
| return ` | |
| <svg viewBox="0 0 ${width} ${height}" aria-label="${escapeHtml(word)} silüeti"> | |
| ${guideLines} | |
| ${cutLines} | |
| <path class="silhouette" d="${outline}" /> | |
| </svg> | |
| `; | |
| } | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """); | |
| } | |
| function render() { | |
| const words = document.getElementById("words").value | |
| .split(/\n+/) | |
| .map(w => w.trim()) | |
| .filter(Boolean); | |
| const options = { | |
| cellWidth: Number(document.getElementById("cellWidth").value) || 28, | |
| heightScale: Number(document.getElementById("heightScale").value) || 1, | |
| copies: Math.max(1, Number(document.getElementById("copies").value) || 1), | |
| labelMode: document.getElementById("labelMode").value, | |
| showGuides: document.getElementById("showGuides").checked, | |
| showLetterCuts: document.getElementById("showLetterCuts").checked, | |
| }; | |
| const paper = document.getElementById("paper"); | |
| paper.innerHTML = ` | |
| <div class="worksheet-title"> | |
| <div>Kelime silüeti çalışması</div> | |
| <div>Ad: <span class="name-line"></span></div> | |
| </div> | |
| `; | |
| words.forEach(word => { | |
| for (let i = 0; i < options.copies; i++) { | |
| const item = document.createElement("div"); | |
| item.className = "item"; | |
| item.innerHTML = ` | |
| ${options.labelMode === "left" ? `<div class="word-label">${escapeHtml(word)}</div>` : ""} | |
| ${makeWordSvg(word, options)} | |
| `; | |
| paper.appendChild(item); | |
| } | |
| }); | |
| } | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment