Skip to content

Instantly share code, notes, and snippets.

@hcivelek
Created May 20, 2026 16:53
Show Gist options
  • Select an option

  • Save hcivelek/abd751239d04105a211e3d3d04630fbf to your computer and use it in GitHub Desktop.

Select an option

Save hcivelek/abd751239d04105a211e3d3d04630fbf to your computer and use it in GitHub Desktop.
Word Writing Examples
<!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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
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