<!doctype html>
<html lang=”am”>
<head>
<meta charset=”utf-8″ />
<meta name=”viewport” content=”width=device-width, initial-scale=1″ />
<title>የፊደል ቤተሰብ ስክራምብል + ድምፅ (33+)</title>
<style>
:root{
–bg:#f4f9ff; –card:#fff; –ink:#17324d; –muted:#5b7088;
–accent:#2f80ed; –good:#27ae60; –bad:#e74c3c;
–shadow:0 10px 25px rgba(0,0,0,.10); –radius:16px;
}
body{
font-family:system-ui,-apple-system,”Noto Sans Ethiopic”,”Nyala”,”Abyssinica SIL”,Segoe UI,Roboto,Arial,sans-serif;
background:var(–bg);
margin:0; padding:18px;
color:var(–ink);
text-align:center;
}
.container{
max-width:880px;
margin:auto;
background:var(–card);
padding:18px;
border-radius:var(–radius);
box-shadow:var(–shadow);
}
h1{margin:4px 0 6px;}
.sub{margin:0 0 12px; color:var(–muted); font-size:14px}
.topbar{
display:flex; flex-wrap:wrap; gap:10px;
align-items:center; justify-content:space-between;
margin:10px 0 8px;
}
.left, .right{display:flex; flex-wrap:wrap; gap:10px; align-items:center}
.pill{
padding:8px 12px; border-radius:999px;
border:2px solid #d9e8ff; background:#fff;
font-weight:800; color:var(–ink);
}
button{
padding:10px 16px;
border:none;
border-radius:999px;
font-weight:900;
cursor:pointer;
background:#eef5ff;
color:var(–ink);
user-select:none;
}
button.primary{ background:var(–accent); color:#fff; }
button.good{ background:var(–good); color:#fff; }
button:disabled{opacity:.5; cursor:not-allowed}
label.toggle{
display:flex; align-items:center; gap:8px;
padding:8px 12px; border-radius:999px;
border:2px solid #d9e8ff; background:#fff;
font-weight:900;
}
label.toggle input{ transform:scale(1.15); }
.familyTitle{
font-size:20px; font-weight:900; margin:10px 0 6px;
}
.targetLine{
color:var(–muted);
font-size:14px;
margin:0 0 12px;
word-spacing:8px;
}
.zones{
display:grid;
grid-template-columns:1fr;
gap:12px;
margin-top:10px;
}
@media (min-width: 860px){
.zones{ grid-template-columns:1fr 1fr; }
}
.panel{
border-radius:var(–radius);
border:2px dashed #cfe3ff;
background:#fbfdff;
padding:14px;
text-align:left;
}
.panel h3{
margin:0 0 10px;
font-size:14px;
color:var(–muted);
font-weight:900;
}
.dropzone{
border-radius:var(–radius);
border:2px solid #d9e8ff;
background:#fff;
padding:12px;
min-height:110px;
}
.dropzone.over{ outline:3px solid rgba(47,128,237,.25); outline-offset:2px; }
.tiles{
display:flex;
flex-wrap:wrap;
gap:10px;
align-content:flex-start;
justify-content:flex-start;
}
.tile{
width:76px;
height:76px;
font-size:34px;
font-weight:900;
display:flex;
align-items:center;
justify-content:center;
border-radius:18px;
background:#fff;
border:2px solid #cfe3ff;
box-shadow:0 6px 14px rgba(10,50,90,.08);
cursor:grab;
user-select:none;
}
.tile:active{ cursor:grabbing; }
.tile.dragging{ opacity:.6; transform:scale(1.03); }
.tile.correct{ background:#e9fff0; border-color:#2ecc71; }
.tile.wrong{ background:#fff0f0; border-color:#e74c3c; }
.status{
margin-top:10px;
font-weight:900;
font-size:14px;
}
.status.good{ color:var(–good); }
.status.bad{ color:var(–bad); }
.hint{ color:var(–muted); font-size:13px; margin-top:6px; }
</style>
</head>
<body>
<div class=”container”>
<h1>የፊደል ቤተሰብ ስክራምብል (33+)</h1>
<p class=”sub”>እያንዳንዱ ቤተሰብ 7 ፊደሎች ናቸው። ይዘው ያንቀሳቅሱ እና በትክክል ቅደም ተከተል ያድርጉ። ፊደል ላይ ጠቅ ሲያደርጉ ድምፅ ይሰማል።</p>
<div class=”topbar”>
<div class=”left”>
<span class=”pill” id=”progressPill”>ቤተሰብ 1 / 34</span>
<span class=”pill” id=”familyPill”>ፊደል: ሀ</span>
</div>
<div class=”right”>
<button id=”prevBtn”>◀ ቀድሞ</button>
<button class=”primary” id=”checkBtn”>አረጋግጥ</button>
<button class=”good” id=”nextBtn”>ቀጣይ ▶</button>
<button id=”shuffleBtn”>ደባልቅ</button>
<button id=”playFamilyBtn”>🔊 ቤተሰብ አንብብ</button>
<label class=”toggle” title=”ድምፅ አብራ/አጥፋ”>
<input type=”checkbox” id=”audioToggle” checked />
ድምፅ
</label>
</div>
</div>
<div class=”familyTitle” id=”familyTitle”>ቤተሰብ: ሀ</div>
<div class=”targetLine” id=”targetLine”></div>
<div class=”zones”>
<div class=”panel”>
<h3>ባንክ (ከዚህ ይዘው ያንቀሳቅሱ)</h3>
<div class=”dropzone” id=”bankZone”><div class=”tiles” id=”bank”></div></div>
<div class=”hint”>ምክር፡ ፊደል ላይ ጠቅ ብለው ወደ ሌላ ቦታ መላክ ይችላሉ።</div>
</div>
<div class=”panel”>
<h3>መስመር (እዚህ ይጥሉ)</h3>
<div class=”dropzone” id=”lineZone”><div class=”tiles” id=”line”></div></div>
<div class=”hint”>ከዚያ “አረጋግጥ” ይጫኑ።</div>
</div>
</div>
<div class=”status” id=”status”></div>
</div>
<script>
(() => {
// — 33+ Fidel families (7 forms each) —
// Includes vowel-carrier family አ and also all common 33 base families; total here = 34 families.
const FAMILIES = [
[“ሀ”,”ሁ”,”ሂ”,”ሃ”,”ሄ”,”ህ”,”ሆ”],
[“ለ”,”ሉ”,”ሊ”,”ላ”,”ሌ”,”ል”,”ሎ”],
[“ሐ”,”ሑ”,”ሒ”,”ሓ”,”ሔ”,”ሕ”,”ሖ”],
[“መ”,”ሙ”,”ሚ”,”ማ”,”ሜ”,”ም”,”ሞ”],
[“ሠ”,”ሡ”,”ሢ”,”ሣ”,”ሤ”,”ሥ”,”ሦ”],
[“ረ”,”ሩ”,”ሪ”,”ራ”,”ሬ”,”ር”,”ሮ”],
[“ሰ”,”ሱ”,”ሲ”,”ሳ”,”ሴ”,”ስ”,”ሶ”],
[“ሸ”,”ሹ”,”ሺ”,”ሻ”,”ሼ”,”ሽ”,”ሾ”],
[“ቀ”,”ቁ”,”ቂ”,”ቃ”,”ቄ”,”ቅ”,”ቆ”],
[“በ”,”ቡ”,”ቢ”,”ባ”,”ቤ”,”ብ”,”ቦ”],
[“ተ”,”ቱ”,”ቲ”,”ታ”,”ቴ”,”ት”,”ቶ”],
[“ቸ”,”ቹ”,”ቺ”,”ቻ”,”ቼ”,”ች”,”ቾ”],
[“ኀ”,”ኁ”,”ኂ”,”ኃ”,”ኄ”,”ኅ”,”ኆ”],
[“ነ”,”ኑ”,”ኒ”,”ና”,”ኔ”,”ን”,”ኖ”],
[“ኘ”,”ኙ”,”ኚ”,”ኛ”,”ኜ”,”ኝ”,”ኞ”],
// vowel carrier (often taught as a “family”)
[“አ”,”ኡ”,”ኢ”,”ኣ”,”ኤ”,”እ”,”ኦ”],
[“ከ”,”ኩ”,”ኪ”,”ካ”,”ኬ”,”ክ”,”ኮ”],
[“ኸ”,”ኹ”,”ኺ”,”ኻ”,”ኼ”,”ኽ”,”ኾ”],
[“ወ”,”ዉ”,”ዊ”,”ዋ”,”ዌ”,”ው”,”ዎ”],
[“ዐ”,”ዑ”,”ዒ”,”ዓ”,”ዔ”,”ዕ”,”ዖ”],
[“ዘ”,”ዙ”,”ዚ”,”ዛ”,”ዜ”,”ዝ”,”ዞ”],
[“ዠ”,”ዡ”,”ዢ”,”ዣ”,”ዤ”,”ዥ”,”ዦ”],
[“የ”,”ዩ”,”ዪ”,”ያ”,”ዬ”,”ይ”,”ዮ”],
[“ደ”,”ዱ”,”ዲ”,”ዳ”,”ዴ”,”ድ”,”ዶ”],
[“ጀ”,”ጁ”,”ጂ”,”ጃ”,”ጄ”,”ጅ”,”ጆ”],
[“ገ”,”ጉ”,”ጊ”,”ጋ”,”ጌ”,”ግ”,”ጎ”],
[“ጠ”,”ጡ”,”ጢ”,”ጣ”,”ጤ”,”ጥ”,”ጦ”],
[“ጨ”,”ጩ”,”ጪ”,”ጫ”,”ጬ”,”ጭ”,”ጮ”],
[“ጰ”,”ጱ”,”ጲ”,”ጳ”,”ጴ”,”ጵ”,”ጶ”],
[“ጸ”,”ጹ”,”ጺ”,”ጻ”,”ጼ”,”ጽ”,”ጾ”],
[“ፀ”,”ፁ”,”ፂ”,”ፃ”,”ፄ”,”ፅ”,”ፆ”],
[“ፈ”,”ፉ”,”ፊ”,”ፋ”,”ፌ”,”ፍ”,”ፎ”],
[“ፐ”,”ፑ”,”ፒ”,”ፓ”,”ፔ”,”ፕ”,”ፖ”]
];
// — Elements —
const bankEl = document.getElementById(“bank”);
const lineEl = document.getElementById(“line”);
const bankZone = document.getElementById(“bankZone”);
const lineZone = document.getElementById(“lineZone”);
const progressPill = document.getElementById(“progressPill”);
const familyPill = document.getElementById(“familyPill”);
const familyTitle = document.getElementById(“familyTitle”);
const targetLine = document.getElementById(“targetLine”);
const statusEl = document.getElementById(“status”);
const prevBtn = document.getElementById(“prevBtn”);
const nextBtn = document.getElementById(“nextBtn”);
const shuffleBtn = document.getElementById(“shuffleBtn”);
const checkBtn = document.getElementById(“checkBtn”);
const playFamilyBtn = document.getElementById(“playFamilyBtn”);
const audioToggle = document.getElementById(“audioToggle”);
let index = 0; // current family index
let draggedId = null;
// — Shuffle helper —
function shuffle(arr){
const a = arr.slice();
for(let i=a.length-1;i>0;i–){
const j = Math.floor(Math.random()*(i+1));
[a[i],a[j]]=[a[j],a[i]];
}
return a;
}
// — Beep sounds (success/fail) —
const audioCtx = (() => {
try { return new (window.AudioContext || window.webkitAudioContext)(); }
catch { return null; }
})();
function beep(type=”ok”){
if(!audioToggle.checked || !audioCtx) return;
const ctx = audioCtx;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
const now = ctx.currentTime;
const freq = type === “ok” ? 740 : 220;
o.frequency.setValueAtTime(freq, now);
g.gain.setValueAtTime(0.0001, now);
g.gain.exponentialRampToValueAtTime(0.22, now + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.18);
o.start(now);
o.stop(now + 0.2);
}
// — Amharic TTS (Web Speech API) —
// Works best if the device/browser has an Amharic voice installed.
function getAmharicVoice(){
const voices = window.speechSynthesis?.getVoices?.() || [];
// Prefer Amharic voices
let v = voices.find(x => (x.lang || “”).toLowerCase().startsWith(“am”));
// Some systems label Ethiopia Amharic differently—still try a sensible fallback:
if(!v) v = voices.find(x => /ethiop/i.test(x.name) || /amhar/i.test(x.name));
return v || null;
}
function speak(text, {rate=0.9, pitch=1.0} = {}){
if(!audioToggle.checked) return;
if(!(“speechSynthesis” in window)){
// If TTS not supported, just beep
beep(“ok”);
return;
}
// Stop current speech
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = “am-ET”;
u.rate = rate;
u.pitch = pitch;
const v = getAmharicVoice();
if(v) u.voice = v;
window.speechSynthesis.speak(u);
}
// Ensure voices load (some browsers load asynchronously)
if(“speechSynthesis” in window){
window.speechSynthesis.onvoiceschanged = () => { /* voices refreshed */ };
}
// — Drag/drop helpers —
function setZoneHover(zone, on){ zone.classList.toggle(“over”, on); }
function allowDrop(zone){
zone.addEventListener(“dragover”, (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = “move”;
setZoneHover(zone, true);
});
zone.addEventListener(“dragleave”, () => setZoneHover(zone, false));
zone.addEventListener(“drop”, (e) => {
e.preventDefault();
setZoneHover(zone, false);
const id = e.dataTransfer.getData(“text/plain”) || draggedId;
const tile = document.getElementById(id);
if(tile){
zone.querySelector(“.tiles”).appendChild(tile);
}
});
}
function clearMarks(){
[…bankEl.children, …lineEl.children].forEach(t => {
t.classList.remove(“correct”,”wrong”);
});
}
function setStatus(msg, type=””){
statusEl.className = “status” + (type ? ” ” + type : “”);
statusEl.textContent = msg;
}
// — Tile creation —
function makeTile(letter){
const div = document.createElement(“div”);
div.className = “tile”;
div.textContent = letter;
div.draggable = true;
div.id = “tile-” + index + “-” + letter + “-” + Math.random().toString(16).slice(2);
div.addEventListener(“dragstart”, (e) => {
draggedId = div.id;
div.classList.add(“dragging”);
e.dataTransfer.setData(“text/plain”, draggedId);
e.dataTransfer.effectAllowed = “move”;
});
div.addEventListener(“dragend”, () => {
div.classList.remove(“dragging”);
draggedId = null;
});
// Click-to-move + speak letter
div.addEventListener(“click”, () => {
// move between zones
if(div.parentElement === bankEl) lineEl.appendChild(div);
else bankEl.appendChild(div);
// speak the clicked letter
speak(letter, {rate: 0.85});
clearMarks();
});
// Right-click / long press alternative not needed—click already speaks
return div;
}
// — Render family —
function render(){
const family = FAMILIES[index];
// UI labels
progressPill.textContent = `ቤተሰብ ${index+1} / ${FAMILIES.length}`;
familyPill.textContent = `ፊደል: ${family[0]}`;
familyTitle.textContent = `ቤተሰብ: ${family[0]}`;
targetLine.textContent = family.join(” “);
prevBtn.disabled = index === 0;
nextBtn.disabled = index === FAMILIES.length – 1;
// Reset zones
bankEl.innerHTML = “”;
lineEl.innerHTML = “”;
clearMarks();
setStatus(“ዝግጁ! ፊደሎችን አስደራጁ።”);
// Add scrambled tiles to bank
shuffle(family).forEach(ch => bankEl.appendChild(makeTile(ch)));
}
// — Check order —
function check(){
const family = FAMILIES[index];
const tiles = […lineEl.children];
if(tiles.length !== family.length){
setStatus(`እባክዎ ሁሉንም ፊደሎች አስቀምጡ (${family.length})`, “bad”);
beep(“bad”);
speak(“እባክዎ ሁሉንም ፊደሎች ያስቀምጡ”, {rate:0.95});
return;
}
let correct = 0;
tiles.forEach((t,i) => {
t.classList.remove(“correct”,”wrong”);
if(t.textContent === family[i]){
t.classList.add(“correct”);
correct++;
}else{
t.classList.add(“wrong”);
}
});
if(correct === family.length){
setStatus(“በጣም ጥሩ! 🎉 ትክክል ነው”, “good”);
beep(“ok”);
speak(“በጣም ጥሩ! ትክክል ነው”, {rate:0.95});
}else{
setStatus(`${correct}/${family.length} ትክክል — እንደገና ይሞክሩ`, “bad”);
beep(“bad”);
speak(“እንደገና ይሞክሩ”, {rate:0.95});
}
}
// — Play whole family (in order) —
async function playFamily(){
const family = FAMILIES[index];
// Speak each letter with small pauses
// (We chain with timeouts because SpeechSynthesisUtterance has no await)
if(!audioToggle.checked) return;
// Cancel any ongoing speech
if(“speechSynthesis” in window) window.speechSynthesis.cancel();
let i = 0;
const speakNext = () => {
if(i >= family.length) return;
const u = new SpeechSynthesisUtterance(family[i]);
u.lang = “am-ET”;
u.rate = 0.82;
const v = getAmharicVoice();
if(v) u.voice = v;
u.onend = () => {
i++;
setTimeout(speakNext, 180);
};
window.speechSynthesis.speak(u);
};
// If TTS unsupported, fallback to beeps
if(!(“speechSynthesis” in window)){
beep(“ok”);
return;
}
speakNext();
}
// — Wire up events —
allowDrop(bankZone);
allowDrop(lineZone);
prevBtn.addEventListener(“click”, () => { index = Math.max(0, index-1); render(); });
nextBtn.addEventListener(“click”, () => { index = Math.min(FAMILIES.length-1, index+1); render(); });
shuffleBtn.addEventListener(“click”, () => render());
checkBtn.addEventListener(“click”, check);
playFamilyBtn.addEventListener(“click”, playFamily);
// Start
render();
})();
</script>
</body>
</html>