Wiskunde Taken

Kies je eigen leerjaar uit.

  • GonioMeester body { font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, sans-serif; } .fade-in { animation: fadeIn 0.35s ease-out both; } .pop-in { animation: popIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } .slide-up { animation: slideUp 0.4s ease-out both; } @keyframes fadeIn { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } } @keyframes popIn { from { opacity:0; transform:scale(0.85); } to { opacity:1; transform:scale(1); } } @keyframes slideUp { from { opacity:0; transform:translateY(24px); } to { opacity:1; transform:translateY(0); } } .progress-bar { transition: width 0.6s ease; } .btn-hover:hover { transform: scale(1.05); } .btn-tap:active { transform: scale(0.95); } .step-fade { transition: opacity 0.4s, transform 0.4s; } .step-fade.inactive { opacity: 0.3; transform: scale(0.97); pointer-events: none; }
    const { useState, useRef } = React; /* ===================== ICONS (inline SVG) ===================== */ const Ic = ({ children, className = “” }) => ( {children} ); const BookOpen = ({className}) => ; const GraduationCap = ({className}) => ; const PlayIcon = ({className}) => ; const ChevronRight = ({className}) => ; const ChevronLeft = ({className}) => ; const HomeIcon = ({className}) => ; const HelpCircle = ({className}) => ; const RotateCcw = ({className}) => ; const CheckCircle2 = ({className}) => ; const AlertCircle = ({className}) => ; const Calculator = ({className}) => ; const InfoIcon = ({className}) => ; const XIcon = ({className}) => ; /* ===================== EXERCISES DATA ===================== */ // Tangens-only oefeningen: driehoeken met correcte verhoudingen (tan = overstaand/aanliggend) // SVG points berekend zodat hoeken kloppen: bv. 5-12-13 → angle ≈ 22,6° const exercisesTan = [ // TAN LEVEL 1: Hoek berekenen. Opg 1-3: alleen overstaand+aanliggend, invoer zijdenaam. Opg 4-7: alle zijdes, leerling kiest. // Alle driehoeken: tangens gebruikt ALLEEN de twee rechthoekszijden (overstaand + aanliggend), NOOIT de schuine zijde. { id:’t1-1′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:0, triangle:{ points:[[50,250],[350,125],[350,250]], labels:{A:’A’,B:’B’,C:’C’}, values:{BC:5,AC:12,angleC:90}, targetAngle:’A’, rightAngle:’C’ }, given:{ sides:{BC:5,AC:12} }, target:{ type:’hoek’, label:’hoek A’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’BC’, aanliggendLabel:’AC’, steps:[“tan(A) = BC / AC = 5 / 12″,”A = tan⁻¹(5/12) ≈ 22,6°”], finalValue:22.6 }, hints:[“Kijk naar hoek A. BC ligt tegenover, AC ligt aan de hoek.”,”BC is overstaand (5), AC is aanliggend (12).”] }, { id:’t1-2′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:90, triangle:{ points:[[50,250],[350,90],[350,250]], labels:{K:’K’,L:’L’,M:’M’}, values:{LM:8,KM:15,angleM:90}, targetAngle:’K’, rightAngle:’M’ }, given:{ sides:{LM:8,KM:15} }, target:{ type:’hoek’, label:’hoek K’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’LM’, aanliggendLabel:’KM’, steps:[“tan(K) = LM / KM = 8 / 15″,”K = tan⁻¹(8/15) ≈ 28,1°”], finalValue:28.1 }, hints:[“Kijk naar hoek K. LM ligt tegenover, KM ligt aan de hoek.”,”LM is overstaand (8), KM is aanliggend (15).”] }, { id:’t1-3′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:180, triangle:{ points:[[50,250],[350,40],[350,250]], labels:{P:’P’,Q:’Q’,R:’R’}, values:{QR:7,PR:10,angleR:90}, targetAngle:’P’, rightAngle:’R’ }, given:{ sides:{QR:7,PR:10} }, target:{ type:’hoek’, label:’hoek P’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’QR’, aanliggendLabel:’PR’, steps:[“tan(P) = QR / PR = 7 / 10″,”P = tan⁻¹(7/10) ≈ 35,0°”], finalValue:35.0 }, hints:[“QR is overstaand (7), PR is aanliggend (10).”,”tan(P) = 7 / 10″] }, { id:’t1-4′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:90, triangle:{ points:[[50,250],[350,25],[350,250]], labels:{D:’D’,E:’E’,F:’F’}, values:{EF:3,DF:4,angleF:90}, targetAngle:’D’, rightAngle:’F’ }, given:{ sides:{EF:3,DF:4} }, target:{ type:’hoek’, label:’hoek D’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’EF’, aanliggendLabel:’DF’, steps:[“tan(D) = EF / DF = 3 / 4″,”D = tan⁻¹(3/4) ≈ 36,9°”], finalValue:36.9 }, hints:[“EF is overstaand (3), DF is aanliggend (4).”] }, { id:’t1-5′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:45, triangle:{ points:[[50,250],[350,100],[350,250]], labels:{X:’X’,Y:’Y’,Z:’Z’}, values:{YZ:9,XZ:12,angleZ:90}, targetAngle:’X’, rightAngle:’Z’ }, given:{ sides:{YZ:9,XZ:12} }, target:{ type:’hoek’, label:’hoek X’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’YZ’, aanliggendLabel:’XZ’, steps:[“tan(X) = YZ / XZ = 9 / 12″,”X = tan⁻¹(9/12) ≈ 36,9°”], finalValue:36.9 }, hints:[“YZ is overstaand (9), XZ is aanliggend (12).”] }, { id:’t1-6′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:0, triangle:{ points:[[50,250],[200,50],[50,50]], labels:{S:’S’,T:’T’,U:’U’}, values:{TU:12,SU:16,angleU:90}, targetAngle:’S’, rightAngle:’U’ }, given:{ sides:{TU:12,SU:16} }, target:{ type:’hoek’, label:’hoek S’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’TU’, aanliggendLabel:’SU’, steps:[“tan(S) = TU / SU = 12 / 16″,”S = tan⁻¹(12/16) ≈ 36,9°”], finalValue:36.9 }, hints:[“TU is overstaand (12), SU is aanliggend (16).”] }, { id:’t1-7′, level:1, type:’hoek’, mode:’tan’, inputMode:’type’, rotation:0, triangle:{ points:[[350,250],[50,25],[50,250]], labels:{G:’G’,H:’H’,I:’I’}, values:{HI:6,GI:8,angleI:90}, targetAngle:’G’, rightAngle:’I’ }, given:{ sides:{HI:6,GI:8} }, target:{ type:’hoek’, label:’hoek G’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’HI’, aanliggendLabel:’GI’, steps:[“tan(G) = HI / GI = 6 / 8″,”G = tan⁻¹(6/8) ≈ 36,9°”], finalValue:36.9 }, hints:[“HI is overstaand (6), GI is aanliggend (8).”] }, // TAN LEVEL 2: Zijde berekenen (alleen tangens, overstaand+aanliggend) { id:’t2-1′, level:2, type:’zijde’, mode:’tan’, rotation:90, triangle:{ points:[[50,250],[350,125],[350,250]], labels:{A:’A’,B:’B’,C:’C’}, values:{BC:’?’,AC:12,AB:13,angleA:22.6,angleC:90}, targetAngle:’A’, rightAngle:’C’ }, given:{ angle:22.6, sides:{AC:12} }, target:{ type:’zijde’, label:’BC’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’BC’, aanliggendLabel:’AC’, steps:[“tan(22,6°) = BC / 12″,”BC = tan(22,6°) × 12″,”BC ≈ 5”], finalValue:5 }, hints:[“BC is overstaand, AC is aanliggend. tan(22,6°) = BC / 12”] }, { id:’t2-2′, level:2, type:’zijde’, mode:’tan’, rotation:180, triangle:{ points:[[50,250],[350,90],[350,250]], labels:{K:’K’,L:’L’,M:’M’}, values:{LM:’?’,KM:15,KL:17,angleK:28.1,angleM:90}, targetAngle:’K’, rightAngle:’M’ }, given:{ angle:28.1, sides:{KM:15} }, target:{ type:’zijde’, label:’LM’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’LM’, aanliggendLabel:’KM’, steps:[“tan(28,1°) = LM / 15″,”LM = tan(28,1°) × 15″,”LM ≈ 8”], finalValue:8 }, hints:[“LM is overstaand, KM is aanliggend.”] }, { id:’t2-3′, level:2, type:’zijde’, mode:’tan’, rotation:270, triangle:{ points:[[50,250],[350,40],[350,250]], labels:{P:’P’,Q:’Q’,R:’R’}, values:{QR:’?’,PR:10,PQ:12.2,angleP:35,angleR:90}, targetAngle:’P’, rightAngle:’R’ }, given:{ angle:35, sides:{PR:10} }, target:{ type:’zijde’, label:’QR’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’QR’, aanliggendLabel:’PR’, steps:[“tan(35°) = QR / 10″,”QR = tan(35°) × 10″,”QR ≈ 7”], finalValue:7 }, hints:[“QR is overstaand, PR is aanliggend.”] }, { id:’t2-4′, level:2, type:’zijde’, mode:’tan’, rotation:0, triangle:{ points:[[50,250],[350,25],[350,250]], labels:{D:’D’,E:’E’,F:’F’}, values:{EF:’?’,DF:4,DE:5,angleD:36.9,angleF:90}, targetAngle:’D’, rightAngle:’F’ }, given:{ angle:36.9, sides:{DF:4} }, target:{ type:’zijde’, label:’EF’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’EF’, aanliggendLabel:’DF’, steps:[“tan(36,9°) = EF / 4″,”EF = tan(36,9°) × 4″,”EF ≈ 3”], finalValue:3 }, hints:[“EF is overstaand, DF is aanliggend.”] }, { id:’t2-5′, level:2, type:’zijde’, mode:’tan’, rotation:135, triangle:{ points:[[50,250],[350,125],[350,250]], labels:{A:’A’,B:’B’,C:’C’}, values:{AC:’?’,BC:5,AB:13,angleA:22.6,angleC:90}, targetAngle:’A’, rightAngle:’C’ }, given:{ angle:22.6, sides:{BC:5} }, target:{ type:’zijde’, label:’AC’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, overstaandLabel:’BC’, aanliggendLabel:’AC’, steps:[“tan(22,6°) = 5 / AC”,”AC = 5 / tan(22,6°)”,”AC ≈ 12″], finalValue:12 }, hints:[“BC is overstaand, AC is aanliggend. Gebruik de 6-3-2 regel: AC = 5 ÷ tan(22,6°).”] }, ]; const exercises = [ // —- LEVEL 1: Hoek berekenen —- { id:’l1-1′, level:1, type:’hoek’, rotation:0, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{A:’A’,B:’B’,C:’C’}, values:{BC:6,AB:10,angleC:90}, targetAngle:’A’, rightAngle:’C’ }, given:{ sides:{BC:6,AB:10} }, target:{ type:’hoek’, label:’hoek A’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“Kijk vanuit hoek A.”,”BC is overstaand (6).”,”AB is schuin (10).”,”sin(A) = 6/10″,”A = sin⁻¹(0,6) ≈ 36,9°”], finalValue:36.9 }, hints:[“Kijk naar hoek A.”,”BC is overstaand, AB is schuin.”,”Gebruik SOS.”] }, { id:’l1-2′, level:1, type:’hoek’, rotation:90, triangle:{ points:[[50,250],[350,100],[50,100]], labels:{K:’K’,M:’M’,L:’L’}, values:{KL:12,LM:5,angleL:90}, targetAngle:’K’, rightAngle:’L’ }, given:{ sides:{KL:12,LM:5} }, target:{ type:’hoek’, label:’hoek K’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“Kijk vanuit hoek K.”,”LM is overstaand (5).”,”KL is aanliggend (12).”,”tan(K) = 5/12″,”K = tan⁻¹(5/12) ≈ 22,6°”], finalValue:22.6 }, hints:[“Kijk naar hoek K.”,”LM is overstaand, KL is aanliggend.”,”Gebruik TOA.”] }, { id:’l1-3′, level:1, type:’hoek’, rotation:180, triangle:{ points:[[350,150],[50,250],[350,250]], labels:{Z:’Z’,X:’X’,Y:’Y’}, values:{XZ:12,YZ:9,angleY:90}, targetAngle:’Z’, rightAngle:’Y’ }, given:{ sides:{XZ:12,YZ:9} }, target:{ type:’hoek’, label:’hoek Z’ }, solution:{ ratio:’cos’, sides:{numerator:’aanliggend’,denominator:’schuin’}, steps:[“Kijk vanuit hoek Z.”,”YZ is aanliggend (9).”,”XZ is schuin (12).”,”cos(Z) = 9/12 = 0,75″,”Z = cos⁻¹(0,75) ≈ 41,4°”], finalValue:41.4 }, hints:[“Kijk naar hoek Z.”,”YZ is aanliggend, XZ is schuin.”,”Gebruik CAS.”] }, { id:’l1-4′, level:1, type:’hoek’, rotation:270, triangle:{ points:[[300,250],[50,50],[50,250]], labels:{R:’R’,P:’P’,Q:’Q’}, values:{PQ:8,QR:15,angleQ:90}, targetAngle:’R’, rightAngle:’Q’ }, given:{ sides:{PQ:8,QR:15} }, target:{ type:’hoek’, label:’hoek R’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“Kijk vanuit hoek R.”,”PQ is overstaand (8).”,”QR is aanliggend (15).”,”tan(R) = 8/15″,”R = tan⁻¹(8/15) ≈ 28,1°”], finalValue:28.1 }, hints:[“Kijk naar hoek R.”,”PQ is overstaand, QR is aanliggend.”,”Gebruik TOA.”] }, { id:’l1-5′, level:1, type:’hoek’, rotation:45, triangle:{ points:[[300,250],[50,100],[50,250]], labels:{B:’B’,C:’C’,A:’A’}, values:{AC:7,BC:12,angleA:90}, targetAngle:’B’, rightAngle:’A’ }, given:{ sides:{AC:7,BC:12} }, target:{ type:’hoek’, label:’hoek B’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“Kijk vanuit hoek B.”,”AC is overstaand (7).”,”BC is schuin (12).”,”sin(B) = 7/12″,”B = sin⁻¹(7/12) ≈ 35,7°”], finalValue:35.7 }, hints:[“Kijk naar hoek B.”,”AC is overstaand, BC is schuin.”,”Gebruik SOS.”] }, { id:’l1-6′, level:1, type:’hoek’, rotation:135, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{D:’D’,F:’F’,E:’E’}, values:{DE:14,DF:18,angleE:90}, targetAngle:’D’, rightAngle:’E’ }, given:{ sides:{DE:14,DF:18} }, target:{ type:’hoek’, label:’hoek D’ }, solution:{ ratio:’cos’, sides:{numerator:’aanliggend’,denominator:’schuin’}, steps:[“Kijk vanuit hoek D.”,”DE is aanliggend (14).”,”DF is schuin (18).”,”cos(D) = 14/18″,”D = cos⁻¹(14/18) ≈ 38,9°”], finalValue:38.9 }, hints:[“Kijk naar hoek D.”,”DE is aanliggend, DF is schuin.”,”Gebruik CAS.”] }, { id:’l1-7′, level:1, type:’hoek’, rotation:0, triangle:{ points:[[50,250],[350,100],[350,250]], labels:{S:’S’,U:’U’,T:’T’}, values:{TU:10,ST:24,angleT:90}, targetAngle:’S’, rightAngle:’T’ }, given:{ sides:{TU:10,ST:24} }, target:{ type:’hoek’, label:’hoek S’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“Kijk vanuit hoek S.”,”TU is overstaand (10).”,”ST is aanliggend (24).”,”tan(S) = 10/24″,”S = tan⁻¹(10/24) ≈ 22,6°”], finalValue:22.6 }, hints:[“Kijk naar hoek S.”,”TU is overstaand, ST is aanliggend.”,”Gebruik TOA.”] }, // —- LEVEL 2: Zijde berekenen —- { id:’l2-1′, level:2, type:’zijde’, rotation:90, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{A:’A’,B:’B’,C:’C’}, values:{BC:8,AB:’?’,angleA:30,angleC:90}, targetAngle:’A’, rightAngle:’C’ }, given:{ angle:30, sides:{BC:8} }, target:{ type:’zijde’, label:’AB’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“sin(30°) = 8 / AB”,”AB = 8 / sin(30°)”,”AB = 8 / 0,5 = 16″], finalValue:16 }, hints:[“Kijk vanuit hoek A.”,”BC is overstaand, AB is schuin.”,”Gebruik SOS en de 6-3-2 regel.”] }, { id:’l2-2′, level:2, type:’zijde’, rotation:180, triangle:{ points:[[50,250],[350,100],[50,100]], labels:{K:’K’,M:’M’,L:’L’}, values:{KL:12,LM:’?’,angleK:25,angleL:90}, targetAngle:’K’, rightAngle:’L’ }, given:{ angle:25, sides:{KL:12} }, target:{ type:’zijde’, label:’LM’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(25°) = LM / 12″,”LM = tan(25°) * 12″,”LM ≈ 0,466 * 12 ≈ 5,6”], finalValue:5.6 }, hints:[“Kijk vanuit hoek K.”,”LM is overstaand, KL is aanliggend.”,”Gebruik TOA en de 6-3-2 regel.”] }, { id:’l2-3′, level:2, type:’zijde’, rotation:270, triangle:{ points:[[350,150],[50,250],[350,250]], labels:{Z:’Z’,X:’X’,Y:’Y’}, values:{XZ:20,YZ:’?’,angleZ:40,angleY:90}, targetAngle:’Z’, rightAngle:’Y’ }, given:{ angle:40, sides:{XZ:20} }, target:{ type:’zijde’, label:’YZ’ }, solution:{ ratio:’cos’, sides:{numerator:’aanliggend’,denominator:’schuin’}, steps:[“cos(40°) = YZ / 20″,”YZ = cos(40°) * 20″,”YZ ≈ 0,766 * 20 ≈ 15,3”], finalValue:15.3 }, hints:[“Kijk vanuit hoek Z.”,”YZ is aanliggend, XZ is schuin.”,”Gebruik CAS.”] }, { id:’l2-4′, level:2, type:’zijde’, rotation:45, triangle:{ points:[[300,250],[50,50],[50,250]], labels:{R:’R’,P:’P’,Q:’Q’}, values:{PQ:’?’,PR:25,angleR:35,angleQ:90}, targetAngle:’R’, rightAngle:’Q’ }, given:{ angle:35, sides:{PR:25} }, target:{ type:’zijde’, label:’PQ’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“sin(35°) = PQ / 25″,”PQ = sin(35°) * 25″,”PQ ≈ 0,574 * 25 ≈ 14,3”], finalValue:14.3 }, hints:[“Kijk vanuit hoek R.”,”PQ is overstaand, PR is schuin.”,”Gebruik SOS.”] }, { id:’l2-5′, level:2, type:’zijde’, rotation:135, triangle:{ points:[[300,250],[50,100],[50,250]], labels:{B:’B’,C:’C’,A:’A’}, values:{AC:10,BC:’?’,angleB:50,angleA:90}, targetAngle:’B’, rightAngle:’A’ }, given:{ angle:50, sides:{AC:10} }, target:{ type:’zijde’, label:’BC’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“sin(50°) = 10 / BC”,”BC = 10 / sin(50°)”,”BC ≈ 10 / 0,766 ≈ 13,1″], finalValue:13.1 }, hints:[“Kijk vanuit hoek B.”,”AC is overstaand, BC is schuin.”,”Gebruik SOS.”] }, { id:’l2-6′, level:2, type:’zijde’, rotation:0, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{D:’D’,F:’F’,E:’E’}, values:{DE:’?’,DF:30,angleD:20,angleE:90}, targetAngle:’D’, rightAngle:’E’ }, given:{ angle:20, sides:{DF:30} }, target:{ type:’zijde’, label:’DE’ }, solution:{ ratio:’cos’, sides:{numerator:’aanliggend’,denominator:’schuin’}, steps:[“cos(20°) = DE / 30″,”DE = cos(20°) * 30″,”DE ≈ 0,940 * 30 ≈ 28,2”], finalValue:28.2 }, hints:[“Kijk vanuit hoek D.”,”DE is aanliggend, DF is schuin.”,”Gebruik CAS.”] }, { id:’l2-7′, level:2, type:’zijde’, rotation:90, triangle:{ points:[[50,250],[350,100],[350,250]], labels:{S:’S’,U:’U’,T:’T’}, values:{ST:18,TU:’?’,angleS:55,angleT:90}, targetAngle:’S’, rightAngle:’T’ }, given:{ angle:55, sides:{ST:18} }, target:{ type:’zijde’, label:’TU’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(55°) = TU / 18″,”TU = tan(55°) * 18″,”TU ≈ 1,428 * 18 ≈ 25,7”], finalValue:25.7 }, hints:[“Kijk vanuit hoek S.”,”TU is overstaand, ST is aanliggend.”,”Gebruik TOA.”] }, // —- LEVEL 3: Gemengd —- { id:’l3-1′, level:3, type:’zijde’, rotation:180, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{D:’D’,F:’F’,E:’E’}, values:{DE:15,EF:’?’,DF:17,angleD:35,angleE:90}, targetAngle:’D’, rightAngle:’E’ }, given:{ angle:35, sides:{DE:15} }, target:{ type:’zijde’, label:’EF’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(35°) = EF / 15″,”EF = tan(35°) * 15″,”EF ≈ 10,5”], finalValue:10.5 }, hints:[“Kijk vanuit hoek D.”,”DE is aanliggend, EF is overstaand.”,”Gebruik TOA.”] }, { id:’l3-2′, level:3, type:’hoek’, rotation:270, triangle:{ points:[[350,100],[50,250],[350,250]], labels:{C:’C’,A:’A’,B:’B’}, values:{AB:12,BC:5,AC:13,angleB:90}, targetAngle:’C’, rightAngle:’B’ }, given:{ sides:{AB:12,BC:5} }, target:{ type:’hoek’, label:’hoek C’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(C) = 12 / 5 = 2,4″,”C = tan⁻¹(2,4) ≈ 67,4°”], finalValue:67.4 }, hints:[“Kijk vanuit hoek C.”,”AB is overstaand, BC is aanliggend.”,”Gebruik TOA.”] }, { id:’l3-3′, level:3, type:’zijde’, rotation:45, triangle:{ points:[[50,100],[300,250],[50,250]], labels:{Z:’Z’,Y:’Y’,X:’X’}, values:{XY:’?’,XZ:14,YZ:18,angleX:90,angleZ:50}, targetAngle:’Z’, rightAngle:’X’ }, given:{ angle:50, sides:{XZ:14} }, target:{ type:’zijde’, label:’XY’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(50°) = XY / 14″,”XY = tan(50°) * 14″,”XY ≈ 16,7”], finalValue:16.7 }, hints:[“Kijk vanuit hoek Z.”,”XY is overstaand, XZ is aanliggend.”,”Gebruik TOA.”] }, { id:’l3-4′, level:3, type:’hoek’, rotation:135, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{K:’K’,M:’M’,L:’L’}, values:{KL:20,KM:25,LM:15,angleL:90}, targetAngle:’K’, rightAngle:’L’ }, given:{ sides:{KL:20,KM:25} }, target:{ type:’hoek’, label:’hoek K’ }, solution:{ ratio:’cos’, sides:{numerator:’aanliggend’,denominator:’schuin’}, steps:[“cos(K) = 20 / 25 = 0,8″,”K = cos⁻¹(0,8) ≈ 36,9°”], finalValue:36.9 }, hints:[“Kijk vanuit hoek K.”,”KL is aanliggend, KM is schuin.”,”Gebruik CAS.”] }, { id:’l3-5′, level:3, type:’zijde’, rotation:0, triangle:{ points:[[350,150],[50,250],[350,250]], labels:{P:’P’,R:’R’,Q:’Q’}, values:{PQ:22,QR:’?’,PR:26,angleQ:90,angleP:42}, targetAngle:’P’, rightAngle:’Q’ }, given:{ angle:42, sides:{PQ:22} }, target:{ type:’zijde’, label:’QR’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(42°) = QR / 22″,”QR = tan(42°) * 22″,”QR ≈ 19,8”], finalValue:19.8 }, hints:[“Kijk vanuit hoek P.”,”QR is overstaand, PQ is aanliggend.”,”Gebruik TOA.”] }, { id:’l3-6′, level:3, type:’hoek’, rotation:90, triangle:{ points:[[300,250],[50,50],[50,250]], labels:{C:’C’,A:’A’,B:’B’}, values:{AB:10,BC:24,AC:26,angleB:90}, targetAngle:’C’, rightAngle:’B’ }, given:{ sides:{AB:10,AC:26} }, target:{ type:’hoek’, label:’hoek C’ }, solution:{ ratio:’sin’, sides:{numerator:’overstaand’,denominator:’schuin’}, steps:[“sin(C) = 10 / 26″,”C = sin⁻¹(10/26) ≈ 22,6°”], finalValue:22.6 }, hints:[“Kijk vanuit hoek C.”,”AB is overstaand, AC is schuin.”,”Gebruik SOS.”] }, { id:’l3-7′, level:3, type:’zijde’, rotation:180, triangle:{ points:[[50,250],[350,50],[350,250]], labels:{U:’U’,S:’S’,T:’T’}, values:{ST:’?’,TU:12,SU:15,angleT:90,angleU:53}, targetAngle:’U’, rightAngle:’T’ }, given:{ angle:53, sides:{TU:12} }, target:{ type:’zijde’, label:’ST’ }, solution:{ ratio:’tan’, sides:{numerator:’overstaand’,denominator:’aanliggend’}, steps:[“tan(53°) = ST / 12″,”ST = tan(53°) * 12″,”ST ≈ 15,9”], finalValue:15.9 }, hints:[“Kijk vanuit hoek U.”,”ST is overstaand, TU is aanliggend.”,”Gebruik TOA.”] } ]; /* ===================== TRIANGLE VISUAL ===================== */ function TriangleVisual({ exercise, highlightedSides = [], showLabels = true }) { const { points, labels, values, targetAngle, rightAngle } = exercise.triangle; const rot = exercise.rotation || 0; const cx = (points[0][0]+points[1][0]+points[2][0])/3, cy = (points[0][1]+points[1][1]+points[2][1])/3; const pA = points[0], pB = points[1], pC = points[2]; const labelKeys = Object.keys(labels); const scale = rot ? 0.82 : 1; const vbCx = 200, vbCy = 150; const transformPoint = (p) => { if (!rot) return p; const rad = rot * Math.PI / 180; const cos = Math.cos(rad), sin = Math.sin(rad); const dx = (p[0] – cx) * scale, dy = (p[1] – cy) * scale; return [vbCx + dx*cos – dy*sin, vbCy + dx*sin + dy*cos]; }; const getMidpoint = (p1, p2) => [(p1[0]+p2[0])/2, (p1[1]+p2[1])/2]; const getPointByLabel = (lbl) => points[labelKeys.indexOf(lbl)]; const pRight = getPointByLabel(rightAngle); const pTarget = getPointByLabel(targetAngle); const getAngle = (p1, p2) => Math.atan2(p2[1]-p1[1], p2[0]-p1[0]); const getSideLabel = (p1Idx, p2Idx) => { const ri = labelKeys.indexOf(rightAngle); const ti = labelKeys.indexOf(targetAngle); if (p1Idx !== ri && p2Idx !== ri) return ‘schuin’; if (p1Idx !== ti && p2Idx !== ti) return ‘overstaand’; if ((p1Idx===ti&&p2Idx===ri)||(p1Idx===ri&&p2Idx===ti)) return ‘aanliggend’; return null; }; const getLabelPos = (p1, p2) => { const mid = getMidpoint(p1, p2); const dx = p2[0]-p1[0], dy = p2[1]-p1[1]; const len = Math.sqrt(dx*dx+dy*dy); let nx = -dy/len, ny = dx/len; const tp = points.find(p => p !== p1 && p !== p2); if (nx*(tp[0]-mid[0]) + ny*(tp[1]-mid[1]) > 0) { nx=-nx; ny=-ny; } return [mid[0]+nx*25, mid[1]+ny*25]; }; const geomTransform = rot ? `translate(${vbCx},${vbCy}) scale(${scale}) rotate(${rot}) translate(${-cx},${-cy})` : undefined; const rightAngleMarker = (() => { const ri = labelKeys.indexOf(rightAngle); const others = [0,1,2].filter(i=>i!==ri); const p1 = points[others[0]], p2 = points[others[1]]; const v1x=p1[0]-pRight[0], v1y=p1[1]-pRight[1]; const v2x=p2[0]-pRight[0], v2y=p2[1]-pRight[1]; const l1=Math.sqrt(v1x*v1x+v1y*v1y), l2=Math.sqrt(v2x*v2x+v2y*v2y); const s=15; const u1x=(v1x/l1)*s, u1y=(v1y/l1)*s; const u2x=(v2x/l2)*s, u2y=(v2y/l2)*s; return ; })(); const targetArcData = (() => { const ti = labelKeys.indexOf(targetAngle); const others = [0,1,2].filter(i=>i!==ti); const p1=points[others[0]], p2=points[others[1]]; let a1=getAngle(pTarget,p1), a2=getAngle(pTarget,p2); let diff=a2-a1; while(diff Math.PI) diff-=2*Math.PI; const r=32; const sx=pTarget[0]+Math.cos(a1)*r, sy=pTarget[1]+Math.sin(a1)*r; const ex=pTarget[0]+Math.cos(a1+diff)*r, ey=pTarget[1]+Math.sin(a1+diff)*r; const sweep=diff>0?1:0; const tr=r+20, mid=a1+diff/2; const tx=pTarget[0]+Math.cos(mid)*tr, ty=pTarget[1]+Math.sin(mid)*tr; const angleText=exercise.given.angle?`${String(exercise.given.angle).replace(‘.’,’,’)}°`:’?°’; return { path: `M ${sx} ${sy} A ${r} ${r} 0 0 ${sweep} ${ex} ${ey}`, tx, ty, angleText }; })(); const sideLabelData = showLabels ? [ {p1:points[0],p2:points[1],val:values[labelKeys[0]+labelKeys[1]]||values[labelKeys[1]+labelKeys[0]],type:getSideLabel(0,1)}, {p1:points[1],p2:points[2],val:values[labelKeys[1]+labelKeys[2]]||values[labelKeys[2]+labelKeys[1]],type:getSideLabel(1,2)}, {p1:points[0],p2:points[2],val:values[labelKeys[0]+labelKeys[2]]||values[labelKeys[2]+labelKeys[0]],type:getSideLabel(0,2)}, ] : []; const pointLabelData = points.map((p,i)=>({ p, lbl: Object.values(labels)[i], i })); return (
    p.join(‘,’)).join(‘ ‘)} fill=”none” stroke=”black” strokeWidth=”2″/> {rightAngleMarker} {pointLabelData.map(({p,lbl,i})=>{ const mid=points.reduce((acc,c)=>[acc[0]+c[0]/3,acc[1]+c[1]/3],[0,0]); const dx=p[0]-mid[0], dy=p[1]-mid[1]; const len=Math.sqrt(dx*dx+dy*dy); const isTargetVertex = lbl===targetAngle; const offset = isTargetVertex ? 30 : 22; const pos = transformPoint([p[0]+(dx/len)*offset, p[1]+(dy/len)*offset]); return {lbl}; })} {sideLabelData.map((side,i)=>{ if(side.val===undefined) return null; const pos = transformPoint(getLabelPos(side.p1,side.p2)); const hi=side.type&&highlightedSides.includes(side.type); return ( {side.val} {hi&&side.type&&({side.type})} ); })} {(()=>{ const pos = transformPoint([targetArcData.tx, targetArcData.ty]); return {targetArcData.angleText}; })()}
    ); } /* ===================== TAN FRACTION BUILDER (Level 1 tangens) ===================== */ function TanFractionBuilder({ exercise, overstaandVal, aanliggendVal, onCorrect }) { const [topVal, setTopVal] = useState(null); const [bottomVal, setBottomVal] = useState(null); const [feedback, setFeedback] = useState(null); const correctTop = overstaandVal; const correctBottom = aanliggendVal; const opts = [correctTop, correctBottom]; const handleSlot = (slot, val) => { if(slot===’top’){ setTopVal(val); if(bottomVal!==null) check(val, bottomVal); } else{ setBottomVal(val); if(topVal!==null) check(topVal, val); } }; const check = (t,b) => { if(Number(t)===correctTop && Number(b)===correctBottom){ setFeedback({ok:true}); setTimeout(onCorrect, 1500); } else { setFeedback({ok:false}); setTimeout(()=>{ setTopVal(null); setBottomVal(null); setFeedback(null); }, 2000); } }; return (

    Vul de formule in met de juiste getallen. Zet overstaand boven de streep en aanliggend eronder.

    tan({exercise.target.label.replace(‘hoek ‘,”)}) =
    overstaand
    {topVal||’?’}
    {bottomVal||’?’}
    aanliggend
    {opts.map((v,i)=>( ))}
    {feedback&&(
    {feedback.ok? Perfect! De breuk staat goed. Overstaand boven, aanliggend onder. Probeer opnieuw. )}
    ); } /* ===================== TAN SIDE FRACTION BUILDER (Level 2 tangens) ===================== */ // Vul verhouding in: hoek (graden), onbekende zijde = zijdenaam, bekende zijde = getal function TanSideFractionBuilder({ exercise, onCorrect }) { const [angleVal, setAngleVal] = useState(”); const [numVal, setNumVal] = useState(”); const [denVal, setDenVal] = useState(”); const [feedback, setFeedback] = useState(null); const sol = exercise.solution; const ovLbl = (sol.overstaandLabel||”).toUpperCase().replace(/\s/g,”); const avLbl = (sol.aanliggendLabel||”).toUpperCase().replace(/\s/g,”); const knownSides = exercise.given.sides || {}; const ovNum = knownSides[ovLbl] ?? knownSides[ovLbl.split(”).reverse().join(”)]; const avNum = knownSides[avLbl] ?? knownSides[avLbl.split(”).reverse().join(”)]; const correctAngle = exercise.given.angle; const norm = (s) => (s||”).trim().toUpperCase().replace(/\s/g,”); const check = () => { const a = angleVal.trim().replace(‘,’,’.’); const n = norm(numVal), d = norm(denVal); const aNum = parseFloat(a); const angleOk = !isNaN(aNum) && Math.abs(aNum – correctAngle) setFeedback(null), 2000); return; } if (!angleOk) { setFeedback({ok:false, msg:”De hoek klopt niet. Vul het aantal graden in (bijv. 22,6).”}); setTimeout(()=>{ setAngleVal(”); setFeedback(null); }, 2500); return; } if (nOk && dOk) { setFeedback({ok:true}); setTimeout(onCorrect, 1500); } else { setFeedback({ok:false, msg:”Nog niet goed. Onbekende zijde = letters (bijv. BC). Bekende zijde = getal.”}); setTimeout(()=>{ setNumVal(”); setDenVal(”); setFeedback(null); }, 2500); } }; return (

    Vul in: hoek (graden), zijdenaam of getal. Onbekend = letters, bekend = getal.

    tan(
    hoek° setAngleVal(e.target.value)} placeholder=”22,6″ className=”w-14 h-11 rounded-xl border-2 border-amber-300 text-center text-base font-bold focus:border-amber-500 outline-none” onKeyDown={e=>e.key===’Enter’&&check()}/>
    °) =
    overst. setNumVal(e.target.value)} placeholder=”BC” className=”w-14 h-11 rounded-xl border-2 border-amber-300 text-center text-base font-bold focus:border-amber-500 outline-none” onKeyDown={e=>e.key===’Enter’&&check()}/>
    /
    aanlig. setDenVal(e.target.value)} placeholder=”12″ className=”w-14 h-11 rounded-xl border-2 border-amber-300 text-center text-base font-bold focus:border-amber-500 outline-none” onKeyDown={e=>e.key===’Enter’&&check()}/>
    {feedback&&(
    {feedback.ok? Perfect! {feedback.msg} )}
    ); } /* ===================== FORMULA CARD ===================== */ const colorMap = { emerald:{ border:’#059669′, text:’#065f46′ }, amber: { border:’#f59e0b’, text:’#92400e’ }, blue: { border:’#3b82f6′, text:’#1e40af’ }, red: { border:’#ef4444′, text:’#991b1b’ }, }; function FormulaCard({ title, content, color=”emerald” }) { const c = colorMap[color]||colorMap.emerald; return (

    {title}

    {content}
    ); } /* ===================== FORMULA BUILDER ===================== */ // chosenRatio: door leerling gekozen ratio (sin/cos/tan) // knownValue: numerieke waarde van de bekende zijde // isMultiply: true = ×, false = ÷ function FormulaBuilder({ exercise, chosenRatio, knownValue, isMultiply, onCorrect }) { const [slots, setSlots] = useState([null,null,null]); const [feedback, setFeedback] = useState(null); const targetSide = exercise.target.label; const angle = exercise.given.angle; const ratioPart = `${chosenRatio}(${angle}°)`; const knownStr = String(knownValue); const options = [ratioPart, knownStr, ‘×’, ‘÷’]; const checkFormula = (currentSlots) => { if(currentSlots.includes(null)) return; const [p1,op,p2] = currentSlots; let correct = false; if(isMultiply && op===’×’ && ((p1===ratioPart&&p2===knownStr)||(p1===knownStr&&p2===ratioPart))) correct=true; if(!isMultiply && op===’÷’ && p1===knownStr && p2===ratioPart) correct=true; if(correct){ setFeedback({ msg:”Perfect! De formule staat goed.”, ok:true }); setTimeout(onCorrect, 1500); } else { setFeedback({ msg:”Nog niet helemaal. Denk aan de 6-3-2 regel.”, ok:false }); setTimeout(()=>{ setSlots([null,null,null]); setFeedback(null); }, 2000); } }; const handleClick = (opt) => { const newSlots=[…slots]; const isOp = opt===’×’||opt===’÷’; if(isOp){ newSlots[1]=opt; } else { const empty = newSlots[0]===null?0:(newSlots[2]===null?2:-1); if(empty!==-1) newSlots[empty]=opt; } setSlots(newSlots); if(!newSlots.includes(null)) checkFormula(newSlots); }; return (

    6
    De 6-3-2 Regel

    3
    {chosenRatio.toUpperCase()}
    =
    6
    2

    Zoek je de BOVENKANT (6)?

    3 × 2

    Zoek je de ONDERKANT (2)?

    6 ÷ 3

    {targetSide} =
    {[0,1,2].map(i=>(
    {slots[i]||(i===1?’?’:’…’)}
    ))}

    Klik de onderdelen in de vakjes

    {options.map((opt,i)=>( ))}
    {feedback && (
    {feedback.ok?:} {feedback.msg}
    )}
    ); } /* ===================== HEADER ===================== */ function Header({ onHome, onBackToChoice }) { return (

    GonioMeester

    {onBackToChoice && ( )}
    ); } /* ===================== MAIN APP ===================== */ function App() { const [mode, setMode] = useState(‘choice’); // ‘choice’ | ’tan’ | ‘all’ const [view, setView] = useState(‘home’); const [level, setLevel] = useState(1); const [exIdx, setExIdx] = useState(0); const [degWarn, setDegWarn] = useState(true); const [step, setStep] = useState(‘IDENTIFY_SIDES’); const [sel, setSel] = useState({ sides:[], ratio:null, calculation:”, sideInputOverstaand:”, sideInputAanliggend:” }); const [fb, setFb] = useState({ message:”, type:null }); const [showHint, setShowHint] = useState(false); const [hintIdx, setHintIdx] = useState(0); const [showNumberPopup, setShowNumberPopup] = useState(false); const isTanMode = mode === ’tan’; const exercisePool = isTanMode ? exercisesTan : exercises; const levelExercises = exercisePool.filter(e=>e.level===level); const currentEx = levelExercises[exIdx] || levelExercises[0]; const reset = () => { setStep(‘IDENTIFY_SIDES’); setSel({ sides:[], ratio:null, calculation:”, sideInputOverstaand:”, sideInputAanliggend:” }); setShowNumberPopup(false); setFb({ message:”, type:null }); setShowHint(false); setHintIdx(0); }; const handleLevelSelect = (l) => { setLevel(l); setExIdx(0); reset(); setView(‘exercise’); }; const handleModeSelect = (m) => { setMode(m); setLevel(1); setExIdx(0); reset(); setView(‘home’); }; /* —- HELPERS: dynamische zijde-waarden en ratio-matching —- */ // Geeft een map terug van { overstaand: waarde, aanliggend: waarde, schuin: waarde } // op basis van de driehoeksdata van de opgave. function getSideValues(ex) { const { points, labels, values, targetAngle, rightAngle } = ex.triangle; const lk = Object.keys(labels); const ri = lk.indexOf(rightAngle); const ti = lk.indexOf(targetAngle); const result = {}; [[0,1],[1,2],[0,2]].forEach(([i,j]) => { const isHyp = i !== ri && j !== ri; const isOpp = i !== ti && j !== ti; const type = isHyp ? ‘schuin’ : isOpp ? ‘overstaand’ : ‘aanliggend’; const v = values[lk[i]+lk[j]] !== undefined ? values[lk[i]+lk[j]] : values[lk[j]+lk[i]]; if (v !== undefined && v !== ‘?’ && String(v) !== ‘?’) result[type] = parseFloat(v); }); return result; } // Geeft terug welke zijdes wel een waarde hebben in de driehoek (getal of ?) – alleen die tonen bij stap 1 function getVisibleSides(ex) { const { points, labels, values, targetAngle, rightAngle } = ex.triangle; const lk = Object.keys(labels); const ri = lk.indexOf(rightAngle); const ti = lk.indexOf(targetAngle); const result = []; [[0,1],[1,2],[0,2]].forEach(([i,j]) => { const isHyp = i !== ri && j !== ri; const isOpp = i !== ti && j !== ti; const type = isHyp ? ‘schuin’ : isOpp ? ‘overstaand’ : ‘aanliggend’; const v = values[lk[i]+lk[j]] !== undefined ? values[lk[i]+lk[j]] : values[lk[j]+lk[i]]; if (v !== undefined) result.push(type); // heeft getal of ? }); return result; } // Geeft de juiste verhouding terug voor een paar zijdes function getRatioForSides(sides) { const s = sides.slice().sort().join(‘-‘); if (s === ‘overstaand-schuin’) return ‘sin’; if (s === ‘aanliggend-schuin’) return ‘cos’; if (s === ‘aanliggend-overstaand’) return ’tan’; return null; } // Geeft terug of de formule een × of ÷ is op basis van doelzijde en bekende zijde function getOperation(targetType, knownType) { if ((targetType===’overstaand’ && knownType===’schuin’) || (targetType===’aanliggend’ && knownType===’schuin’) || (targetType===’overstaand’ && knownType===’aanliggend’)) return ‘×’; return ‘÷’; } // Bereken de verwachte uitkomst voor een zijde-opgave op basis van gekozen aanpak function computeZijdeAnswer(angle, targetType, knownType, knownVal) { const r = angle * Math.PI / 180; if (targetType===’overstaand’ && knownType===’schuin’) return knownVal * Math.sin(r); if (targetType===’aanliggend’ && knownType===’schuin’) return knownVal * Math.cos(r); if (targetType===’overstaand’ && knownType===’aanliggend’) return knownVal * Math.tan(r); if (targetType===’schuin’ && knownType===’overstaand’) return knownVal / Math.sin(r); if (targetType===’schuin’ && knownType===’aanliggend’) return knownVal / Math.cos(r); if (targetType===’aanliggend’ && knownType===’overstaand’) return knownVal / Math.tan(r); return null; } const nextStep = () => { const tanHoekSteps = [‘IDENTIFY_SIDES’,’WRITE_FORMULA’,’CALCULATE’,’FINISHED’]; const tanZijdeSteps = [‘IDENTIFY_SIDES’,’WRITE_FORMULA_ZIJDE’,’REARRANGE’,’CALCULATE’,’FINISHED’]; const normalSteps = [‘IDENTIFY_SIDES’,’CHOOSE_RATIO’,’REARRANGE’,’CALCULATE’,’FINISHED’]; const steps = (isTanMode && currentEx.type===’hoek’) ? tanHoekSteps : (isTanMode && level===2 && currentEx.type===’zijde’) ? tanZijdeSteps : normalSteps; let ni = steps.indexOf(step)+1; if(steps[ni]===’REARRANGE’ && currentEx.type===’hoek’) ni++; if(ni({…p, ratio:’tan’, sides:[‘overstaand’,’aanliggend’]})); setFb({ message:”, type:null }); }; const checkSides = (side) => { if(sel.sides.includes(side)){ setSel(p=>({…p, sides:p.sides.filter(s=>s!==side)})); return; } const newSides=[…sel.sides,side]; setSel(p=>({…p, sides:newSides})); if(newSides.length===2){ const sv = getSideValues(currentEx); const visibleSides = (!isTanMode&&(level===2||level===3)&&currentEx.type===’zijde’) ? getVisibleSides(currentEx) : null; if(currentEx.type===’hoek’){ if(newSides.every(s=>sv[s]!==undefined)){ setFb({ message:”Goed zo! Je hebt de juiste zijdes gekozen.”, type:’success’ }); setTimeout(nextStep, 1500); } else { setFb({ message:”Niet helemaal. Kies twee zijdes waarvan je de waarde weet.”, type:’error’ }); setTimeout(()=>{ setSel(p=>({…p,sides:[]})); setFb({message:”,type:null}); }, 2000); } } else { // Bij level 2/3 “alles”: alleen zijdes uit het figuur (getal of ?) goedkeuren if(visibleSides && !newSides.every(s=>visibleSides.includes(s))){ setFb({ message:”Kies alleen zijdes die in het figuur staan (met getal of ?).”, type:’error’ }); setTimeout(()=>{ setSel(p=>({…p,sides:[]})); setFb({message:”,type:null}); }, 2000); return; } // Zijde-opgaven: één onbekende (de doelzijde) + één bekende const unknownCount = newSides.filter(s=>sv[s]===undefined).length; const knownCount = newSides.filter(s=>sv[s]!==undefined).length; if(unknownCount===1 && knownCount===1){ setFb({ message:”Goed zo! Je hebt de juiste zijdes gekozen.”, type:’success’ }); setTimeout(nextStep, 1500); } else if(unknownCount===0){ setFb({ message:”Kies ook de onbekende zijde die je wilt berekenen.”, type:’error’ }); setTimeout(()=>{ setSel(p=>({…p,sides:[]})); setFb({message:”,type:null}); }, 2000); } else { setFb({ message:”Niet helemaal. Kies één bekende en één onbekende zijde.”, type:’error’ }); setTimeout(()=>{ setSel(p=>({…p,sides:[]})); setFb({message:”,type:null}); }, 2000); } } } }; const checkSideInput = () => { const ov = (sel.sideInputOverstaand||”).trim().toUpperCase().replace(/\s/g,”); const av = (sel.sideInputAanliggend||”).trim().toUpperCase().replace(/\s/g,”); const hasNumbers = /\d/.test(ov) || /\d/.test(av); if(hasNumbers){ setShowNumberPopup(true); return; } const correctOv = (currentEx.solution.overstaandLabel||”).toUpperCase().replace(/\s/g,”); const correctAv = (currentEx.solution.aanliggendLabel||”).toUpperCase().replace(/\s/g,”); if(!ov||!av){ setFb({ message:”Vul beide zijdenamen in.”, type:’error’ }); return; } const ovOk = (ov===correctOv||ov===correctOv.split(”).reverse().join(”)); const avOk = (av===correctAv||av===correctAv.split(”).reverse().join(”)); if(ovOk&&avOk){ setSel(p=>({…p,sides:[‘overstaand’,’aanliggend’]})); setFb({ message:`Goed! ${currentEx.solution.overstaandLabel} is overstaand, ${currentEx.solution.aanliggendLabel} is aanliggend.`, type:’success’ }); setTimeout(nextStep, 1500); } else { setFb({ message:”Niet helemaal. Kijk goed: welke zijde ligt tegenover de hoek? Welke ligt aan de hoek?”, type:’error’ }); setTimeout(()=>setFb({message:”,type:null}), 2500); } }; const checkRatio = (ratio) => { setSel(p=>({…p,ratio})); // De juiste verhouding wordt altijd bepaald door de gekozen zijdes (SOS/CAS/TOA) const expectedRatio = getRatioForSides(sel.sides); if(ratio===expectedRatio){ setFb({ message:`Correct! Voor ${sel.sides[0]} en ${sel.sides[1]} gebruik je ${ratio.toUpperCase()}.`, type:’success’ }); setTimeout(nextStep, 1500); } else { setFb({ message:`Nee, denk aan SOS CAS TOA. Welke regel hoort bij ${sel.sides[0]} en ${sel.sides[1]}?`, type:’error’ }); setTimeout(()=>{ setSel(p=>({…p,ratio:null})); setFb({message:”,type:null}); }, 2000); } }; const checkCalculation = () => { const val=parseFloat(sel.calculation.replace(‘,’,’.’)); if(isNaN(val)){ setFb({ message:”Vul een geldig getal in.”, type:’error’ }); return; } const sv = getSideValues(currentEx); let expected = currentEx.solution.finalValue; if(currentEx.type===’hoek’ && sel.sides.length===2 && sel.ratio){ const o=sv[‘overstaand’], a=sv[‘aanliggend’], s=sv[‘schuin’]; const R2D = 180/Math.PI; if(sel.ratio===’sin’ && o!==undefined && s!==undefined) expected = Math.asin(o/s)*R2D; if(sel.ratio===’cos’ && a!==undefined && s!==undefined) expected = Math.acos(a/s)*R2D; if(sel.ratio===’tan’ && o!==undefined && a!==undefined) expected = Math.atan(o/a)*R2D; } else if(currentEx.type===’zijde’ && sel.sides.length===2 && sel.ratio){ const targetType = sel.sides.find(s=>sv[s]===undefined); const knownType = sel.sides.find(s=>sv[s]!==undefined); if(targetType && knownType){ const computed = computeZijdeAnswer(currentEx.given.angle, targetType, knownType, sv[knownType]); if(computed!==null) expected = computed; } } if(Math.abs(val – expected) < 0.5){ setFb({ message:"Helemaal goed! Nu nog even afronden.", type:'success' }); setTimeout(nextStep, 1500); } else { setFb({ message:"Dat klopt niet helemaal. Heb je de juiste formule gebruikt? Staat je rekenmachine op GRADEN (DEG)?", type:'error' }); } }; /* —- STEP NUMBER LABEL —- */ const stepNum = (isTanMode&&currentEx.type==='hoek') ? (step==='IDENTIFY_SIDES'?1:step==='WRITE_FORMULA'?2:step==='CALCULATE'?3:step==='FINISHED'?'✓':'') : (isTanMode&&level===2&&currentEx.type==='zijde') ? (step==='IDENTIFY_SIDES'?1:step==='WRITE_FORMULA_ZIJDE'?2:step==='REARRANGE'?3:step==='CALCULATE'?4:step==='FINISHED'?'✓':'') : (step==='IDENTIFY_SIDES'?1:step==='CHOOSE_RATIO'?2:step==='REARRANGE'?3:step==='CALCULATE'?(currentEx.type==='hoek'?3:4):step==='FINISHED'?'✓':''); /* —- MODE CHOICE VIEW (eerste scherm) —- */ if(view==='choice' || mode==='choice') return (

    GonioMeester

    Waar wil je mee oefenen?

    Kies wat bij jou past.

    ); /* —- HOME VIEW —- */ if(view===’home’) return (
    setView(‘home’)} onBackToChoice={mode!==’choice’?()=>setMode(‘choice’):null}/>

    {isTanMode?’Tangens oefenen’:’Goniometrie voor Beginners’}

    {isTanMode?’Oefen stap voor stap met de tangens. Eerst hoeken berekenen, daarna zijdes.’ :’Welkom bij GonioMeester. We gaan stap voor stap leren hoe je hoeken en zijdes berekent in een rechthoekige driehoek.’}

    {(isTanMode ? [ { id:1, title:”Level 1″, desc:”Hoek berekenen met tangens”, icon:, color:”bg-amber-500″ }, { id:2, title:”Level 2″, desc:”Zijde berekenen met tangens”, icon:, color:”bg-amber-600″ }, ] : [ { id:1, title:”Level 1″, desc:”Hoeken berekenen”, icon:, color:”bg-blue-500″ }, { id:2, title:”Level 2″, desc:”Zijdes berekenen”, icon:, color:”bg-emerald-500″ }, { id:3, title:”Level 3″, desc:”Alles door elkaar”, icon:, color:”bg-purple-500″ }, ]).map((l,i)=>(
    handleLevelSelect(l.id)} className=”bg-white p-8 rounded-3xl shadow-xl border border-gray-100 cursor-pointer group hover:shadow-2xl transition-all btn-hover btn-tap fade-in” style={{animationDelay:`${i*0.1}s`}}>
    {l.icon}

    {l.title}

    {l.desc}

    Starten
    ))}
    {degWarn&&(
    Let op: Zet je rekenmachine op GRADEN (DEG)!
    )}
    ); /* —- THEORY VIEW —- */ if(view===’theory’) return (
    setView(‘home’)} onBackToChoice={mode!==’choice’?()=>setMode(‘choice’):null}/>

    De Basis van Goniometrie

    De Namen van de Zijdes

    In een rechthoekige driehoek hebben de zijdes namen die afhangen van de hoek waar je naar kijkt.

    SOS CAS TOA

    Dit is het belangrijkste ezelsbruggetje om te onthouden welke formule je nodig hebt.

    SOS
    Sinus = Overstaand / Schuin
    CAS
    Cosinus = Aanliggend / Schuin
    TOA
    Tangens = Overstaand / Aanliggend

    De 6-3-2 Regel

    Als je een zijde moet berekenen, moet je de formule vaak omschrijven. Gebruik dit simpele voorbeeld als hulp:

    6 = 3 × 2
    Zoek je de bovenkant? (6)

    Dan doe je de andere twee keer elkaar:
    3 × 2 = 6

    Zoek je de onderkant? (2)

    Dan deel je de bovenkant door de rest:
    6 ÷ 3 = 2

    ); /* —- EXAMPLES VIEW —- */ if(view===’examples’) return (
    setView(‘home’)} onBackToChoice={mode!==’choice’?()=>setMode(‘choice’):null}/>

    Uitgewerkte Voorbeelden

    {[ { title:”Hoek berekenen (Level 1)”, desc:”Je weet de overstaande zijde (6) en de schuine zijde (10).”, steps:[“Stap 1: Welke zijdes heb je? Overstaand en Schuin.”,”Stap 2: Welke regel? SOS (Sinus = Overstaand / Schuin).”,”Stap 3: Invullen: sin(A) = 6 / 10.”,”Stap 4: Hoek berekenen: A = sin⁻¹(0,6).”,”Stap 5: Antwoord: hoek A ≈ 36,9°.”] }, { title:”Zijde berekenen (Level 2) – Bovenkant”, desc:”Je weet hoek A (30°) en de aanliggende zijde (10). Je zoekt de overstaande zijde.”, steps:[“Stap 1: Welke regel? TOA (tan(30°) = overstaand / 10).”,”Stap 2: Omschrijven met 6-3-2: 6 = 3 * 2.”,”Stap 3: overstaand = tan(30°) * 10.”,”Stap 4: Uitkomst: overstaand ≈ 0,577 * 10 ≈ 5,8.”] }, { title:”Zijde berekenen (Level 2) – Onderkant”, desc:”Je weet hoek A (40°) en de overstaande zijde (5). Je zoekt de aanliggende zijde.”, steps:[“Stap 1: Welke regel? TOA (tan(40°) = 5 / aanliggend).”,”Stap 2: Omschrijven met 6-3-2: 2 = 6 / 3.”,”Stap 3: aanliggend = 5 / tan(40°).”,”Stap 4: Uitkomst: aanliggend ≈ 5 / 0,839 ≈ 6,0.”] } ].map((ex,i)=>(

    {ex.title}

    {ex.desc}

    {ex.steps.map((s,j)=>(
    {s}
    ))}
    ))}
    ); /* —- EXERCISE VIEW —- */ const totalEx = levelExercises.length; return (
    setView(‘home’)} onBackToChoice={mode!==’choice’?()=>setMode(‘choice’):null}/>
    {/* Nav bar */}
    Level {level} Opgave {exIdx+1} van {totalEx}
    {levelExercises.map((_,i)=>(
    ))}
    {/* Left: Triangle + buttons */}

    {currentEx.type===’hoek’?’Bereken ‘:’Bereken zijde ‘} {currentEx.target.label}.

    {showHint&&(

    Hint {hintIdx+1}

    {currentEx.hints[hintIdx]}

    {hintIdx<currentEx.hints.length-1&&( )}
    )}
    {/* Right: Stepplan (breder bij Tangens level 2) */}

    Stappenplan

    Stap {stepNum}
    {/* Step 1 */}
    1

    {((isTanMode&&currentEx.type===’hoek’) || (isTanMode&&level===2&&currentEx.type===’zijde’)) ?’Vul de naam in van de overstaande en aanliggende zijde’ :’Welke zijdes weet je?’}

    {step===’IDENTIFY_SIDES’&&(
    {((isTanMode&&currentEx.type===’hoek’) || (isTanMode&&level===2&&currentEx.type===’zijde’)) ? (

    Typ de letters van de zijde (bijv. BC of CB), niet de getallen.

    setSel(p=>({…p,sideInputOverstaand:e.target.value}))} placeholder=”bijv. BC” className=”w-full p-4 border-2 border-gray-200 rounded-2xl focus:border-emerald-500 outline-none font-mono text-lg” onKeyDown={e=>e.key===’Enter’&&checkSideInput()}/>
    setSel(p=>({…p,sideInputAanliggend:e.target.value}))} placeholder=”bijv. AC” className=”w-full p-4 border-2 border-gray-200 rounded-2xl focus:border-emerald-500 outline-none font-mono text-lg” onKeyDown={e=>e.key===’Enter’&&checkSideInput()}/>
    {(isTanMode&&level===2 ? [‘overstaand’,’aanliggend’] : [‘overstaand’,’aanliggend’,’schuin’]).map(s=>( ))}

    {isTanMode&&level===2 ? ‘Selecteer overstaand en aanliggend (alleen tangens).’ : (!isTanMode&&(level===2||level===3)&&currentEx.type===’zijde’) ? ‘Selecteer twee zijdes. Alleen zijdes uit het figuur (getal of ?) worden goedgekeurd.’ : ‘Selecteer de twee zijdes die je gaat gebruiken.’}

    )}
    {/* Step 2: WRITE_FORMULA (alleen tangens Level 1) */} {(isTanMode&&currentEx.type===’hoek’)&&(
    2

    Vul de formule in met de juiste getallen

    {step===’WRITE_FORMULA’&&(()=>{ const sv = getSideValues(currentEx); return ; })()}
    )} {/* Step 2: WRITE_FORMULA_ZIJDE (alleen tangens Level 2) */} {(isTanMode&&level===2&&currentEx.type===’zijde’)&&(
    2

    Vul in: hoek (°), overstaand en aanliggend

    {step===’WRITE_FORMULA_ZIJDE’&&( )}
    )} {/* Step 2: CHOOSE_RATIO (niet bij tangens Level 1, niet bij tangens Level 2) */} {!(isTanMode&&currentEx.type===’hoek’)&&!(isTanMode&&level===2&&currentEx.type===’zijde’)&&(
    2

    Welke verhouding?

    {step===’CHOOSE_RATIO’&&(
    {[‘sin’,’cos’,’tan’].map(r=>( ))}
    )}
    )} {/* Step 3: Rearrange (Level 2/3, zijde only) */} {(level===2||level===3)&&currentEx.type===’zijde’&&(
    3

    Formule omschrijven

    {step===’REARRANGE’&&(()=>{ const sv = getSideValues(currentEx); const targetType = sel.sides.find(s=>sv[s]===undefined) || currentEx.solution.sides.numerator; const knownType = sel.sides.find(s=>sv[s]!==undefined) || currentEx.solution.sides.denominator; const knownVal = sv[knownType] ?? Object.values(currentEx.given.sides)[0]; const op = getOperation(targetType, knownType); return ; })()}
    )} {/* Step 4: Calculate */}
    {(isTanMode&&currentEx.type===’hoek’)?3:(level===1?3:4)}

    Reken het uit

    {step===’CALCULATE’&&(()=>{ const sv4 = getSideValues(currentEx); let formulaLine = null; if(currentEx.type===’zijde’ && sel.sides.length===2){ const tgt4 = sel.sides.find(s=>sv4[s]===undefined); const kn4 = sel.sides.find(s=>sv4[s]!==undefined); if(tgt4 && kn4){ const op4 = getOperation(tgt4, kn4); const kv4 = sv4[kn4]; const r4 = sel.ratio||’?’; const ang4 = currentEx.given.angle; formulaLine = `${currentEx.target.label} = ${kv4} ${op4} ${r4}(${ang4}°)`; } } else if(currentEx.type===’hoek’ && sel.ratio){ const o=sv4[‘overstaand’], a=sv4[‘aanliggend’], s=sv4[‘schuin’]; const r4=sel.ratio, lbl4=currentEx.target.label; if(r4===’sin’&&o!==undefined&&s!==undefined) formulaLine=`${lbl4} = sin⁻¹(${o} / ${s})`; else if(r4===’cos’&&a!==undefined&&s!==undefined) formulaLine=`${lbl4} = cos⁻¹(${a} / ${s})`; else if(r4===’tan’&&o!==undefined&&a!==undefined) formulaLine=`${lbl4} = tan⁻¹(${o} / ${a})`; } return (
    {(isTanMode&&currentEx.type===’hoek’&&level===1&&(exIdx===0||exIdx===1)) ? (

    Gebruik tan⁻¹ op je rekenmachine:

    1. Deel eerst overstaand door aanliggend (bijv. {sv4.overstaand}/{sv4.aanliggend} = {(sv4.overstaand/sv4.aanliggend).toFixed(3).replace(‘.’,’,’)})
    2. Druk op 2nd of SHIFT
    3. Druk daarna op tan (je ziet nu tan⁻¹)
    4. Typ de uitkomst van de breuk (bijv. {(sv4.overstaand/sv4.aanliggend).toFixed(3).replace(‘.’,’,’)})
    5. Druk op = → je krijgt de hoek in graden

    tan⁻¹({(sv4.overstaand/sv4.aanliggend).toFixed(3).replace(‘.’,’,’)}) ≈ {currentEx.solution.finalValue}°

    ) : (isTanMode&&currentEx.type===’hoek’) ? (

    Bereken de hoek met tan⁻¹ op je rekenmachine. Deel overstaand door aanliggend, druk dan 2nd+tan.

    ) : formulaLine ? (

    Bereken met je rekenmachine:

    {formulaLine}

    ) : (

    {currentEx.type===’hoek’ ? `Bereken de hoek met de inverse functie: ${sel.ratio||’sin’}⁻¹(…)` : ‘Bereken de zijde met je rekenmachine.’}

    )}
    setSel(p=>({…p,calculation:e.target.value}))} onKeyDown={e=>e.key===’Enter’&&checkCalculation()} placeholder=”Bijv. 36.9″ className=”flex-grow p-4 border-2 border-gray-100 rounded-2xl focus:border-emerald-500 outline-none text-xl font-bold transition-all shadow-sm”/>

    Rond af op 1 decimaal.

    ); })()}
    {/* Finished */} {step===’FINISHED’&&(

    Lekker bezig!

    Je hebt deze opgave perfect opgelost.

    )}
    {/* Feedback */} {fb.message&&(
    {fb.type===’success’?:fb.type===’error’?:}

    {fb.message}

    )}
    {showNumberPopup&&(
    setShowNumberPopup(false)}>
    e.stopPropagation()}>

    Geen getallen!

    Vul de namen van de zijdes in (de letters, bijv. BC of AC), niet de getallen.

    )} {degWarn&&(
    Let op: Zet je rekenmachine op GRADEN (DEG)!
    )}
    ); } ReactDOM.createRoot(document.getElementById(‘root’)).render();
  • Heksenketel: rekenen met negatieve getallen :root { –accent: #6d28d9; –accent2: #9333ea; –border: #dbe7ff; –shadow: 0 10px 24px rgba(80,70,150,0.12); } * { box-sizing: border-box; } body { margin: 0; font-family: “Segoe UI”, Arial, sans-serif; background: radial-gradient(circle at 15% 10%, #ecf6ff 0%, #f8fbff 45%, #f5efff 100%); color: #1f2a37; line-height: 1.45; } .container { max-width: 860px; margin: 24px auto; padding: 16px; } .card { background: #fff; border: 2px solid var(–border); border-radius: 18px; padding: 24px; box-shadow: var(–shadow); } h1 { margin: 0 0 16px; font-size: clamp(1.4rem, 3vw, 2rem); color: #3f238f; text-align: center; } /* Status pills */ .status { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; font-weight: 600; font-size: 1rem; } .pill { background: #eef2ff; border: 1px solid #cdd8ff; border-radius: 999px; padding: 7px 14px; } /* Vraag */ .question { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 800; color: #351b7a; text-align: center; margin: 0 0 18px; } /* Invulrij */ .input-row { display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: center; } input[type=”text”] { width: 100%; font-size: 1.2rem; padding: 12px 14px; border: 2px solid #c8d5ff; border-radius: 10px; outline: none; } input[type=”text”]:focus { border-color: var(–accent); box-shadow: 0 0 0 4px rgba(109,40,217,0.12); } /* Knoppen */ button { border: none; border-radius: 10px; padding: 12px 18px; font-size: 1rem; font-weight: 700; cursor: pointer; color: #fff; background: linear-gradient(180deg, var(–accent) 0%, var(–accent2) 100%); transition: transform 0.06s ease; min-height: 46px; } button:hover { transform: translateY(-1px); } button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-hint { background: linear-gradient(180deg, #0284c7 0%, #0369a1 100%); } #nextBtn { display: none; margin-top: 10px; } /* Feedback */ .feedback { display: none; margin-top: 12px; padding: 12px 14px; border-radius: 12px; font-size: 1rem; } .feedback.ok { display: block; background: #ecfff2; border: 2px solid #b9f2cb; color: #0d7e45; font-weight: 700; } .feedback.err { display: block; background: #fff8eb; border: 2px solid #ffe3b0; color: #8a5a07; } .feedback.info { display: block; background: #eef6ff; border: 2px solid #cde4ff; color: #1d4f91; } .steps { margin: 8px 0 0; padding-left: 20px; } .steps li { margin: 4px 0; } /* Hint */ .hint { display: none; margin-top: 14px; border: 2px dashed #b8caf9; background: #f3f7ff; border-radius: 12px; padding: 14px; } .hint h2 { margin: 0 0 8px; font-size: 1.1rem; color: #224a96; } .hint h3 { margin: 12px 0 6px; font-size: 1rem; color: #224a96; } .hint ul { margin: 4px 0 10px; padding-left: 22px; } .hint li { margin: 4px 0; } .hint p { margin: 8px 0; } /* Eindscherm */ .result { display: none; margin-top: 16px; border: 2px solid #d4c9ff; background: linear-gradient(135deg, #f5f0ff 0%, #eef6ff 100%); border-radius: 18px; padding: 28px 24px; text-align: center; } .result-title { margin: 0 0 18px; font-size: 1.6rem; font-weight: 900; color: #3f238f; } .result-cijfer { display: inline-block; font-size: clamp(3.5rem, 10vw, 5.5rem); font-weight: 900; color: #fff; background: linear-gradient(135deg, var(–accent), var(–accent2)); border-radius: 20px; padding: 16px 36px; margin: 0 0 18px; box-shadow: 0 8px 24px rgba(109,40,217,0.3); line-height: 1; } .result-score { font-size: clamp(1.2rem, 3vw, 1.6rem); font-weight: 800; color: #3f238f; margin: 0 0 10px; } .result-formula { font-size: 0.95rem; color: #6b7280; margin: 0 0 16px; } .result-boodschap { font-size: clamp(1.1rem, 2.5vw, 1.4rem); font-weight: 700; color: #166534; background: #ecfff2; border: 2px solid #b9f2cb; border-radius: 12px; padding: 12px 18px; margin: 0 0 20px; display: inline-block; } @media (max-width: 680px) { .input-row { grid-template-columns: 1fr; } button { width: 100%; } .result { padding: 20px 14px; } }

    Heksenketel: rekenen met negatieve getallen

    Opgave 1 van 35 Punten: 0 / 35

    Uitleg: twee soorten sommen

    Temperatuursommen (enkel + of −)

    Denk aan een thermometer. De uitkomst stijgt of daalt.

    • Enkel +: het wordt warmer → uitkomst gaat omhoog.
    • Enkel : het wordt kouder → uitkomst gaat omlaag.

    Voorbeeld: 10 − 13 → het wordt 13 graden kouder → uitkomst is −3.

    Heksenketelsommen (tekencombinatie)

    Bij sommen met twee tekens achter elkaar (zoals −+, +−, −−) denk je in blokjes:

    • Warme blokjes = positief, koude blokjes = negatief.
    • In de ketel doen = optellen, uit de ketel halen = aftrekken.
    • Warme blokjes erbij → warmer → omhoog.
    • Koude blokjes erbij → kouder → omlaag.
    • Warme blokjes eruit → kouder → omlaag.
    • Koude blokjes eruit → warmer → omhoog.

    Voorbeeld: 8 − −10 = 18 → je haalt 10 koude blokjes eruit → warmer → omhoog.

    Voorbeeld: 9 − +1 = 8 → je haalt 1 warm blokje eruit → kouder → omlaag.

    Ronde klaar!
    // ── OPGAVEN ────────────────────────────────────────────────────────────── // Eerste 30 originele sommen + 5 nieuwe – + varianten. // Wil je sommen toevoegen? Voeg gewoon een object toe: { som: “…”, antwoord: getal } const opgaven = [ { som: “7 + -12”, antwoord: -5 }, { som: “8 – -10”, antwoord: 18 }, { som: “-6 + 9”, antwoord: 3 }, { som: “15 – 22”, antwoord: -7 }, { som: “-4 – 11”, antwoord: -15 }, { som: “13 + -8”, antwoord: 5 }, { som: “-9 – -7”, antwoord: -2 }, { som: “5 – +10”, antwoord: -5 }, { som: “-12 + 5”, antwoord: -7 }, { som: “18 + -20”, antwoord: -2 }, { som: “-3 – -9”, antwoord: 6 }, { som: “14 – -6”, antwoord: 20 }, { som: “-15 + 4”, antwoord: -11 }, { som: “6 + -6”, antwoord: 0 }, { som: “-8 – 5”, antwoord: -13 }, { som: “20 – -4”, antwoord: 24 }, { som: “-11 + -7”, antwoord: -18 }, { som: “9 – 13”, antwoord: -4 }, { som: “-2 – -8”, antwoord: 6 }, { som: “17 + -3”, antwoord: 14 }, { som: “-14 – -2”, antwoord: -12 }, { som: “4 + -15”, antwoord: -11 }, { som: “-7 + 12”, antwoord: 5 }, { som: “10 – -10”, antwoord: 20 }, { som: “-20 + 9”, antwoord: -11 }, { som: “16 – 19”, antwoord: -3 }, { som: “-5 – -5”, antwoord: 0 }, { som: “3 + -14”, antwoord: -11 }, { som: “-13 – 6”, antwoord: -19 }, { som: “11 + -17”, antwoord: -6 }, // 5 nieuwe – + sommen { som: “9 – +1”, antwoord: 8 }, { som: “-4 – +5”, antwoord: -9 }, { som: “12 – +7”, antwoord: 5 }, { som: “-10 – +3”, antwoord: -13 }, { som: “6 – +8”, antwoord: -2 } ]; const totaal = opgaven.length; let index = 0; let punten = 0; let gecontroleerd = false; const progressEl = document.getElementById(“progress”); const scoreEl = document.getElementById(“score”); const questionEl = document.getElementById(“questionText”); const inputEl = document.getElementById(“answerInput”); const checkBtn = document.getElementById(“checkBtn”); const nextBtn = document.getElementById(“nextBtn”); const feedbackEl = document.getElementById(“feedback”); const hintBtn = document.getElementById(“hintBtn”); const hintBox = document.getElementById(“hintBox”); const gameBox = document.getElementById(“gameBox”); const resultBox = document.getElementById(“resultBox”); const resCijfer = document.getElementById(“resultCijfer”); const resScore = document.getElementById(“resultScore”); const resFormula = document.getElementById(“resultFormula”); const resBoodschap= document.getElementById(“resultBoodschap”); const restartBtn = document.getElementById(“restartBtn”); // ── STATUS ──────────────────────────────────────────────────────────────── function updateStatus() { progressEl.textContent = “Opgave ” + (index + 1) + ” van ” + totaal; scoreEl.textContent = “Punten: ” + punten + ” / ” + totaal; } // ── VRAAG TONEN ─────────────────────────────────────────────────────────── function toonVraag() { gecontroleerd = false; feedbackEl.className = “feedback”; feedbackEl.innerHTML = “”; nextBtn.style.display = “none”; checkBtn.disabled = false; inputEl.disabled = false; inputEl.value = “”; questionEl.textContent = opgaven[index].som + ” = ?”; updateStatus(); inputEl.focus(); } // ── UITLEG BEPALEN ──────────────────────────────────────────────────────── // Detecteer of de som een tekencombinatie heeft (bv. – +, + -, – -, + +). // Dat is het geval als het tweede getal een expliciete + of – als teken heeft // in de originele tekst, dus: “8 – -10”, “9 – +1”, “7 + -12”. // Een enkele operator zonder tekenvoor het tweede getal = temperatuursom: // “15 – 22”, “-6 + 9”, “-8 – 5”. function parseSom(tekst) { const t = tekst.trim(); // Tekencombinatie: tweede getal heeft expliciete sign (+ of -) // Patroon: [getal] [+-] [+-getal] waarbij het tweede getal met + of – begint let m = t.match(/^([+-]?\d+)\s*([+-])\s*([+-]\d+)$/); if (m) return { start: +m[1], op: m[2], tweede: +m[3], combo: true }; // Enkele operator: tweede getal is gewoon positief (geen teken) m = t.match(/^([+-]?\d+)\s*([+-])\s*(\d+)$/); if (m) return { start: +m[1], op: m[2], tweede: +m[3], combo: false }; return null; } // Meervoud/enkelvoud helper voor blokjes function blokjesTekst(n, warm) { const soort = warm ? (n === 1 ? “warm” : “warme”) : (n === 1 ? “koud” : “koude”); const stuk = n === 1 ? “blokje” : “blokjes”; return n + ” ” + soort + ” ” + stuk; } // Uitleg voor HEKSENKETEL-sommen (tekencombinatie aanwezig) function uitlegHeksenketel(d, uitkomst) { const n = Math.abs(d.tweede); const warm = d.tweede > 0; // +n = warm, -n = koud if (d.op === “+”) { // Optellen: blokjes erbij const type = warm ? “warme” : “koude”; const gevolg = warm ? “warmer” : “kouder”; const richt = warm ? “omhoog” : “omlaag”; return [ “Je begint met ” + d.start + “.”, “Je doet er ” + blokjesTekst(n, warm) + ” bij.”, “Meer ” + type + ” blokjes betekent: ” + gevolg + “.”, “Daarom gaat de uitkomst ” + n + ” ” + richt + “.”, “Antwoord: ” + uitkomst + “.” ]; } else { // Aftrekken: blokjes eruit const type = warm ? “warme” : “koude”; const gevolg = warm ? “kouder” : “warmer”; const richt = warm ? “omlaag” : “omhoog”; return [ “Je begint met ” + d.start + “.”, “Je haalt ” + blokjesTekst(n, warm) + ” uit de ketel.”, “Minder ” + type + ” blokjes betekent: ” + gevolg + “.”, “Daarom gaat de uitkomst ” + n + ” ” + richt + “.”, “Antwoord: ” + uitkomst + “.” ]; } } // Uitleg voor TEMPERATUUR-sommen (enkel + of -) function uitlegTemperatuur(d, uitkomst) { const n = d.tweede; // altijd positief bij temperatuursom const warmer = d.op === “+”; return [ “Je begint bij ” + d.start + ” graden.”, warmer ? “Er komen ” + n + ” graden bij.” : “Er gaan ” + n + ” graden af.”, warmer ? “Het wordt ” + n + ” graden warmer.” : “Het wordt ” + n + ” graden kouder.”, “Je komt uit op ” + uitkomst + ” graden.” ]; } // Kies automatisch de juiste uitlegvorm function bouwUitleg(somTekst, uitkomst) { const d = parseSom(somTekst); if (!d) { return [ “Kijk rustig naar de som.”, “Gebruik de regels van de heksenketel.”, “Antwoord: ” + uitkomst + “.” ]; } return d.combo ? uitlegHeksenketel(d, uitkomst) : uitlegTemperatuur(d, uitkomst); } // ── FEEDBACK ────────────────────────────────────────────────────────────── function toonFeedback(type, html) { feedbackEl.className = “feedback ” + type; feedbackEl.innerHTML = html; } // ── CONTROLEER ──────────────────────────────────────────────────────────── function controleer() { if (gecontroleerd) return; const invoer = inputEl.value.replace(/\s+/g, “”); if (!/^[-+]?\d+$/.test(invoer)) { toonFeedback(“info”, “Typ een heel getal, bijvoorbeeld −3, 0 of 12.”); return; } gecontroleerd = true; inputEl.disabled = true; checkBtn.disabled = true; const leerling = Number(invoer); const goed = opgaven[index].antwoord; if (leerling === goed) { punten++; toonFeedback(“ok”, “Goed gedaan!”); } else { const stappen = bouwUitleg(opgaven[index].som, goed) .map(function(s) { return “
  • ” + s + “
  • “; }).join(“”); toonFeedback(“err”, “Niet goed. Geeft niks, kijk naar de stappen:” + “
      ” + stappen + “
    “); } updateStatus(); nextBtn.style.display = “inline-block”; nextBtn.focus(); } // ── VOLGENDE ────────────────────────────────────────────────────────────── function volgende() { if (!gecontroleerd) { toonFeedback(“info”, “Klik eerst op Controleer, dan gaan we verder.”); return; } index++; if (index >= totaal) toonResultaat(); else toonVraag(); } // ── EINDSCHERM ──────────────────────────────────────────────────────────── function toonResultaat() { gameBox.style.display = “none”; resultBox.style.display = “block”; const rawCijfer = (punten / totaal) * 9 + 1; const cijfer = Math.round(rawCijfer * 10) / 10; const cijferTekst = cijfer.toFixed(1).replace(“.”, “,”); resCijfer.textContent = cijferTekst; resScore.textContent = “Je score: ” + punten + ” van ” + totaal + ” punten”; resFormula.textContent = “(” + punten + ” \u00f7 ” + totaal + “) \u00d7 9 + 1 = ” + cijferTekst; const boodschap = cijfer >= 9.0 ? “\uD83C\uDF1F Uitmuntend! Geweldig gedaan!” : cijfer >= 7.0 ? “\uD83D\uDC4F Goed gedaan! Dat ging prima.” : cijfer >= 5.5 ? “\uD83D\uDE0A Voldoende, maar oefen nog even verder.” : “\uD83D\uDCAA Blijf oefenen, je komt er wel!”; resBoodschap.textContent = boodschap; progressEl.textContent = “Opgave ” + totaal + ” van ” + totaal; scoreEl.textContent = “Punten: ” + punten + ” / ” + totaal; } // ── OPNIEUW ─────────────────────────────────────────────────────────────── function opnieuw() { index = 0; punten = 0; gameBox.style.display = “block”; resultBox.style.display = “none”; toonVraag(); } // ── EVENTS ──────────────────────────────────────────────────────────────── hintBtn.addEventListener(“click”, function() { const open = hintBox.style.display === “block”; hintBox.style.display = open ? “none” : “block”; hintBtn.textContent = open ? “Hint” : “Hint verbergen”; }); checkBtn.addEventListener(“click”, controleer); nextBtn.addEventListener(“click”, volgende); restartBtn.addEventListener(“click”, opnieuw); inputEl.addEventListener(“keydown”, function(e) { if (e.key === “Enter”) { e.preventDefault(); controleer(); } }); toonVraag();
  • Lineaire Formules Vereenvoudigen — par. 9.2 /* ── Variables ── */ :root { –bg:#F5F0E8; –bg2:#EDE7DB; –card:#FDFAF5; –card2:#F8F3EB; –border:#DDD5C5; –border2:#EDE7DB; –txt:#2C2820; –muted:#6B6055; –light:#9B8F82; –grn:#4D7A60; –grn-dk:#3A5E49; –grn-lt:#EBF3EE; –amb:#8A6A1F; –amb-bg:#FDF6E3; –amb-bd:#E8D5A0; –ok:#3A6B48; –ok-bg:#EBF5EE; –ok-bd:#A8D4B4; –err:#A63020; –err-bg:#FAEAE8; –err-bd:#E0A89A; –acc:#C07A4A; –acc-lt:#FBF0E7; –sh:0 3px 12px rgba(44,40,32,.10); –r:14px; –rs:9px; –rx:5px; –font:’Nunito’,system-ui,sans-serif; } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} html{font-size:16px} body{font-family:var(–font);background:var(–bg);color:var(–txt);line-height:1.65;min-height:100vh} /* ── Shell ── */ .app{display:flex;flex-direction:column;min-height:100vh} header{background:var(–card);border-bottom:1px solid var(–border);padding:.7rem 1.4rem;display:flex;align-items:center;gap:.9rem;position:sticky;top:0;z-index:100;box-shadow:0 1px 4px rgba(44,40,32,.07);flex-wrap:wrap} .brand{background:none;border:none;cursor:pointer;font-family:var(–font);font-weight:800;font-size:1rem;color:var(–grn);display:flex;align-items:center;gap:.4rem;padding:.2rem .3rem;border-radius:var(–rx)} .brand:hover{opacity:.8} nav{display:flex;gap:.25rem;margin-left:auto;flex-wrap:wrap} .nb{background:none;border:none;cursor:pointer;font-family:var(–font);font-size:.85rem;font-weight:700;color:var(–muted);padding:.38rem .7rem;border-radius:var(–rx);display:flex;align-items:center;gap:.35rem;transition:background .15s,color .15s} .nb:hover{background:var(–bg2);color:var(–txt)} .nb.on{background:var(–grn-lt);color:var(–grn)} .streak{font-weight:700;font-size:.85rem;color:var(–acc);background:var(–acc-lt);padding:.25rem .55rem;border-radius:99px} .nav-sc{background:var(–grn);color:#fff;border-radius:99px;font-size:.72rem;padding:.05rem .38rem} main{flex:1;padding:1.75rem 1.1rem;max-width:700px;margin:0 auto;width:100%} footer{text-align:center;font-size:.76rem;color:var(–light);padding:1rem;border-top:1px solid var(–border2)} /* ── Buttons ── */ .bp{background:var(–grn);color:#fff;border:none;border-radius:var(–rs);padding:.6rem 1.3rem;font-family:var(–font);font-size:.9rem;font-weight:700;cursor:pointer;transition:background .15s} .bp:hover:not(:disabled){background:var(–grn-dk)} .bp:disabled{opacity:.4;cursor:not-allowed} .bs{background:var(–card);color:var(–grn);border:2px solid var(–grn);border-radius:var(–rs);padding:.55rem 1.1rem;font-family:var(–font);font-size:.88rem;font-weight:700;cursor:pointer} .bs:hover{background:var(–grn-lt)} .bh{background:var(–amb-bg);color:var(–amb);border:1.5px solid var(–amb-bd);border-radius:var(–rs);padding:.58rem 1rem;font-family:var(–font);font-size:.85rem;font-weight:700;cursor:pointer} .bh:hover:not(:disabled){background:#faefc5} .bh:disabled{opacity:.4;cursor:not-allowed} .bg{background:none;color:var(–muted);border:none;font-family:var(–font);font-size:.85rem;font-weight:600;cursor:pointer;text-decoration:underline;text-underline-offset:2px} .bg:hover{color:var(–txt)} .sm{padding:.38rem .8rem!important;font-size:.8rem!important} /* ── Home ── */ .home{display:flex;flex-direction:column;gap:1.85rem} .hero{text-align:center;padding:2.2rem 1rem 1.2rem} .hero-ic{font-size:2.8rem;margin-bottom:.65rem} .hero h2{font-size:1.65rem;font-weight:800} .hero-sub{color:var(–muted);font-size:.85rem;margin-top:.25rem} .hero-intro{margin-top:.85rem;color:var(–muted);font-size:.9rem} .fi{font-family:’Courier New’,monospace;background:var(–grn-lt);color:var(–grn-dk);padding:.12rem .38rem;border-radius:4px;font-size:.88rem} .stats{display:flex;gap:.65rem;justify-content:center;flex-wrap:wrap} .sc{display:flex;flex-direction:column;align-items:center;background:var(–card);border:1px solid var(–border);border-radius:var(–rs);padding:.55rem 1rem;min-width:65px;box-shadow:0 1px 4px rgba(44,40,32,.07)} .sv{font-size:1.35rem;font-weight:800;color:var(–grn)} .sl{font-size:.72rem;color:var(–muted);margin-top:.1rem} .diff-title{font-size:.95rem;font-weight:700;color:var(–muted);margin-bottom:.65rem} .diff-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.65rem} .dlv{display:flex;flex-direction:column;align-items:flex-start;gap:.2rem;padding:.8rem .9rem;border:2px solid var(–border);border-radius:var(–r);background:var(–card);cursor:pointer;font-family:var(–font);text-align:left;transition:border-color .15s,background .15s;box-shadow:0 1px 4px rgba(44,40,32,.07)} .dlv.on-b{border-color:#4D7A60;background:#EBF3EE} .dlv.on-n{border-color:#8A6A1F;background:#FDF6E3} .dlv.on-u{border-color:#A63020;background:#FAEAE8} .ll{font-size:.95rem;font-weight:700;color:var(–txt)} .ld{font-size:.75rem;color:var(–muted)} .start-btn{background:var(–grn);color:#fff;border:none;border-radius:var(–r);padding:.8rem 2.4rem;font-family:var(–font);font-size:1rem;font-weight:700;cursor:pointer;box-shadow:var(–sh);transition:background .15s,transform .1s;align-self:center} .start-btn:hover{background:var(–grn-dk);transform:translateY(-1px)} .tips{background:var(–card);border:1px solid var(–border);border-radius:var(–r);padding:1.1rem 1.4rem;box-shadow:0 1px 4px rgba(44,40,32,.07)} .tips h4{font-size:.88rem;font-weight:700;color:var(–muted);margin-bottom:.5rem} .tips ul{padding-left:1.2rem} .tips li{font-size:.85rem;color:var(–muted);margin-bottom:.25rem} /* ── Worked example ── */ .we{background:var(–card2);border:1px solid var(–border);border-radius:var(–r);margin-bottom:1.1rem;overflow:hidden;box-shadow:0 1px 4px rgba(44,40,32,.07)} .we-toggle{width:100%;display:flex;align-items:center;gap:.45rem;padding:.8rem 1.1rem;background:none;border:none;cursor:pointer;font-family:var(–font);font-size:.88rem;font-weight:700;color:var(–muted);text-align:left} .we-toggle:hover{color:var(–txt)} .we-body{padding:0 1.1rem 1.1rem} .we-f{font-family:’Courier New’,monospace;font-size:1.15rem;font-weight:700;color:var(–grn-dk);background:var(–grn-lt);padding:.55rem .9rem;border-radius:var(–rs);margin-bottom:.85rem} .we-steps{display:flex;flex-direction:column;gap:.65rem} .we-st{display:flex;gap:.65rem;align-items:flex-start} .we-sn{background:var(–grn);color:#fff;font-size:.72rem;font-weight:700;padding:.18rem .5rem;border-radius:99px;white-space:nowrap;flex-shrink:0;margin-top:.18rem} .we-sc{font-size:.88rem} .we-sc ul{padding-left:1.1rem;margin-top:.2rem} .we-ans{font-family:’Courier New’,monospace;font-weight:700;font-size:1rem;color:var(–ok);margin-top:.25rem} .we-note{font-size:.78rem;color:var(–muted);margin-top:.75rem;background:var(–amb-bg);border:1px solid var(–amb-bd);border-radius:var(–rx);padding:.45rem .7rem} /* ── Exercise card ── */ .ex-card{background:var(–card);border:1px solid var(–border);border-radius:var(–r);padding:1.4rem;box-shadow:var(–sh)} .ex-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:.45rem} .ex-meta{display:flex;align-items:center;gap:.45rem;flex-wrap:wrap} .ex-num{font-size:.82rem;font-weight:700;color:var(–muted)} .db{font-size:.7rem;font-weight:700;padding:.18rem .5rem;border-radius:99px;text-transform:uppercase;letter-spacing:.03em} .db-b{background:#EBF3EE;color:#3A6B48} .db-n{background:#FDF6E3;color:#8A6A1F} .db-u{background:#FAEAE8;color:#A63020} .ht{font-size:.7rem;font-weight:600;color:var(–muted);background:var(–bg2);padding:.18rem .45rem;border-radius:99px} .sb-badge{font-size:.76rem;color:var(–amb);background:var(–amb-bg);border:1px solid var(–amb-bd);padding:.18rem .5rem;border-radius:99px} .fbox{background:var(–bg2);border:1px solid var(–border);border-radius:var(–rs);padding:.9rem 1.1rem;margin-bottom:1.1rem} .fl{font-size:.75rem;font-weight:700;color:var(–light);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.3rem} .fd{font-family:’Courier New’,monospace;font-size:1.3rem;font-weight:700;color:var(–txt);letter-spacing:.02em} .ans-row{display:flex;flex-direction:column;gap:.65rem;margin-bottom:.9rem} .ans-inp{width:100%;font-family:’Courier New’,monospace;font-size:1.05rem;padding:.65rem .9rem;border:2px solid var(–border);border-radius:var(–rs);background:#fff;color:var(–txt);outline:none;transition:border-color .15s} .ans-inp:focus{border-color:var(–grn)} .ans-btns{display:flex;gap:.55rem;flex-wrap:wrap;align-items:center} /* ── Hint panel ── */ .hint-panel{display:flex;flex-direction:column;gap:.55rem;margin-bottom:.85rem} .hint-title{font-size:.75rem;font-weight:700;color:var(–amb);text-transform:uppercase;letter-spacing:.05em} .hint-card{background:var(–amb-bg);border:1px solid var(–amb-bd);border-radius:var(–rs);padding:.65rem .9rem;display:flex;gap:.6rem;align-items:flex-start} .hn{font-size:.7rem;font-weight:700;background:var(–amb);color:#fff;padding:.14rem .42rem;border-radius:99px;white-space:nowrap;flex-shrink:0;margin-top:.15rem} .hint-card p{font-size:.87rem} .fsol{background:var(–card2);border:1px solid var(–border);border-radius:var(–rs);padding:.9rem 1.1rem;margin-top:.35rem} .fsol h4{font-size:.82rem;font-weight:700;color:var(–muted);margin-bottom:.45rem} .fsol pre{font-family:’Courier New’,monospace;font-size:.8rem;white-space:pre-wrap;line-height:1.8} /* ── Feedback ── */ .fb{display:flex;gap:.75rem;align-items:flex-start;padding:.85rem 1rem;border-radius:var(–rs);margin-bottom:.65rem} .fb-ok{background:var(–ok-bg);border:1.5px solid var(–ok-bd)} .fb-err{background:var(–err-bg);border:1.5px solid var(–err-bd)} .fb-ic{font-size:1.15rem;font-weight:800;flex-shrink:0;margin-top:.05rem} .fb-ok .fb-ic{color:var(–ok)} .fb-err .fb-ic{color:var(–err)} .fb-ok strong{color:var(–ok)} .fb-err strong{color:var(–err)} .fb-ans{font-family:’Courier New’,monospace;font-size:.88rem;margin-top:.22rem;color:var(–ok);font-weight:700} .fb-hlp{font-size:.75rem;color:var(–muted)} .fb-diag{font-size:.85rem;color:var(–txt);margin-top:.25rem} .fb-acts{display:flex;gap:.45rem;flex-wrap:wrap;margin-top:.5rem} /* ── Sub check ── */ .sub-check{background:var(–amb-bg);border:1.5px solid var(–amb-bd);border-radius:var(–r);padding:1.1rem 1.35rem;margin-top:.85rem} .sub-badge{display:inline-block;background:var(–amb);color:#fff;font-size:.78rem;font-weight:700;padding:.22rem .6rem;border-radius:99px;margin-bottom:.45rem} .sub-desc{font-size:.88rem;color:var(–muted);margin-bottom:.85rem} .sub-step{background:var(–card);border:1px solid var(–border);border-radius:var(–rs);padding:.9rem;margin-bottom:.65rem} .sub-step.done{opacity:.65} .sub-lbl{font-size:.75rem;font-weight:700;color:var(–muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.35rem} .sub-form{font-family:’Courier New’,monospace;font-size:1rem;font-weight:700;color:var(–txt);margin-bottom:.5rem} .sub-row{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap} .sub-row label{font-size:.88rem;color:var(–txt)} .sub-inp{width:85px;font-family:’Courier New’,monospace;font-size:.95rem;padding:.35rem .55rem;border:2px solid var(–border);border-radius:var(–rx);background:#fff;outline:none;transition:border-color .15s} .sub-inp:focus{border-color:var(–grn)} .sub-err{font-size:.8rem;color:var(–err);margin-top:.3rem} .sub-ok{font-size:.86rem;color:var(–ok);font-weight:600} .sub-done{background:var(–ok-bg);border:1.5px solid var(–ok-bd);border-radius:var(–r);padding:1.1rem 1.35rem;margin-top:.85rem;display:flex;gap:.75rem;align-items:flex-start} .sub-done-ic{font-size:1.35rem;color:var(–ok)} .next-row{margin-top:.9rem} .btn-next{width:100%;font-size:.95rem!important;padding:.72rem!important;justify-content:center} /* ── Progress ── */ .prog-panel{display:flex;flex-direction:column;gap:1.6rem} .prog-panel h2{font-size:1.35rem;font-weight:800} .prog-sum{display:grid;grid-template-columns:repeat(auto-fit,minmax(85px,1fr));gap:.65rem} .pc{background:var(–card);border:1px solid var(–border);border-radius:var(–r);padding:.9rem .7rem;text-align:center;box-shadow:0 1px 4px rgba(44,40,32,.07)} .pv{display:block;font-size:1.6rem;font-weight:800;color:var(–grn)} .pl{display:block;font-size:.72rem;color:var(–muted);margin-top:.15rem} .sec h3{font-size:.92rem;font-weight:700;color:var(–muted);margin-bottom:.65rem} .skill-list{display:flex;flex-direction:column;gap:.55rem} .sk-row{display:flex;align-items:center;gap:.65rem;font-size:.85rem} .sk-name{min-width:185px;color:var(–txt)} .sk-bw{flex:1;background:var(–bg2);border-radius:99px;height:8px;overflow:hidden} .sk-b{height:100%;border-radius:99px} .sk-good{background:var(–ok)} .sk-ok{background:#C0872A} .sk-low{background:var(–err)} .sk-pct{font-weight:700;min-width:34px;text-align:right} .sk-cnt{font-size:.76rem;color:var(–light);min-width:52px} .hist-list{display:flex;flex-direction:column;gap:.35rem} .hi{display:flex;align-items:center;gap:.55rem;font-size:.83rem;padding:.42rem .7rem;border-radius:var(–rx);background:var(–card);border:1px solid var(–border)} .hi-ok{border-left:3px solid var(–ok)} .hi-err{border-left:3px solid var(–err)} .hi-ic{font-weight:800} .hi-ok .hi-ic{color:var(–ok)} .hi-err .hi-ic{color:var(–err)} .hi-diff{text-transform:capitalize;color:var(–muted)} .hi-hlp{font-size:.7rem;color:var(–muted);background:var(–bg2);padding:.08rem .32rem;border-radius:99px} .hi-time{margin-left:auto;color:var(–light);font-size:.76rem} .prog-empty{color:var(–muted);text-align:center;padding:2rem 0;font-size:.9rem} /* ── Order warning ── */ .order-warn{font-size:.78rem;color:var(–amb);background:var(–amb-bg);border:1px solid var(–amb-bd);border-radius:var(–rx);padding:.35rem .65rem;margin-top:.3rem;} /* ── Auth ── */ .auth-box{background:var(–card);border:1px solid var(–border);border-radius:var(–r);padding:1.75rem;box-shadow:var(–sh);max-width:380px;margin:3rem auto;display:flex;flex-direction:column;gap:.85rem} .auth-box h2{font-size:1.2rem;font-weight:800} .auth-box p{font-size:.88rem;color:var(–muted)} /* ── Maker ── */ .maker-panel{display:flex;flex-direction:column;gap:1.1rem} .maker-panel h2{font-size:1.35rem;font-weight:800} .maker-intro{font-size:.88rem;color:var(–muted)} .maker-form{background:var(–card);border:1px solid var(–border);border-radius:var(–r);padding:1.35rem;box-shadow:0 1px 4px rgba(44,40,32,.07);display:flex;flex-direction:column;gap:1rem} .ff{display:flex;flex-direction:column;gap:.32rem} .ff label{font-size:.85rem;font-weight:700;color:var(–txt)} .ff input,.ff textarea{font-family:var(–font);font-size:.88rem;padding:.58rem .8rem;border:1.5px solid var(–border);border-radius:var(–rs);background:var(–bg2);color:var(–txt);outline:none;transition:border-color .15s;width:100%} .ff input:focus,.ff textarea:focus{border-color:var(–grn);background:#fff} .ff-hint label{font-size:.78rem;color:var(–amb);font-weight:600} .mk-err{font-size:.85rem;color:var(–err);background:var(–err-bg);border:1px solid var(–err-bd);border-radius:var(–rx);padding:.45rem .7rem} .mk-ok{font-size:.85rem;color:var(–ok);background:var(–ok-bg);border:1px solid var(–ok-bd);border-radius:var(–rx);padding:.45rem .7rem} /* ── Teacher ── */ .tc-panel{display:flex;flex-direction:column;gap:1.4rem} .tc-hdr{display:flex;align-items:center;gap:.65rem} .tc-hdr h2{font-size:1.35rem;font-weight:800} .tc-badge{font-size:.73rem;font-weight:700;background:var(–acc-lt);color:var(–acc);padding:.18rem .55rem;border-radius:99px;text-transform:uppercase;letter-spacing:.04em} .tc-acts{display:flex;gap:.65rem;flex-wrap:wrap} .tc-qcard{background:var(–card);border:1px solid var(–border);border-radius:var(–rs);padding:.9rem;margin-bottom:.6rem;box-shadow:0 1px 4px rgba(44,40,32,.07)} .tq-q{font-family:’Courier New’,monospace;font-size:.88rem;font-weight:700} .tq-a{font-size:.83rem;color:var(–ok);font-family:’Courier New’,monospace} .tq-hints{display:flex;flex-direction:column;gap:.15rem;margin:.4rem 0} .tq-h{font-size:.76rem;color:var(–muted)} .tq-ft{display:flex;align-items:center;justify-content:space-between;margin-top:.5rem} .tq-date{font-size:.73rem;color:var(–light)} .tc-table{width:100%;border-collapse:collapse;font-size:.88rem;background:var(–card);border-radius:var(–rs);overflow:hidden;box-shadow:0 1px 4px rgba(44,40,32,.07)} .tc-table td{padding:.55rem .8rem;border-bottom:1px solid var(–border2)} .tc-table tr:last-child td{border-bottom:none} .tc-table td:first-child{color:var(–muted)} .tc-table td:last-child{text-align:right;font-weight:700} .tc-note{font-size:.78rem;color:var(–light);font-style:italic} /* ── Reflection overlay ── */ .refl-overlay{position:fixed;inset:0;background:rgba(44,40,32,.5);display:flex;align-items:center;justify-content:center;z-index:200;padding:1rem} .refl-card{background:var(–card);border-radius:var(–r);padding:1.85rem 2rem;max-width:510px;width:100%;box-shadow:0 8px 32px rgba(44,40,32,.18);display:flex;flex-direction:column;gap:1.1rem} .refl-ic{font-size:2.1rem;text-align:center} .refl-card h2{font-size:1.3rem;font-weight:800;text-align:center} .refl-intro{font-size:.88rem;color:var(–muted);text-align:center} .rf{display:flex;flex-direction:column;gap:.35rem} .rf label{font-size:.85rem;font-weight:700} .rf textarea{font-family:var(–font);font-size:.88rem;padding:.6rem .8rem;border:1.5px solid var(–border);border-radius:var(–rs);resize:vertical;background:var(–bg2);outline:none;transition:border-color .15s;width:100%} .rf textarea:focus{border-color:var(–grn)} .refl-opts{display:flex;flex-direction:column;gap:.35rem;margin-top:.2rem} .ro{display:flex;align-items:center;gap:.55rem;font-size:.85rem;cursor:pointer;padding:.38rem .6rem;border-radius:var(–rx);border:1.5px solid var(–border);background:var(–card);transition:border-color .15s,background .15s} .ro input{margin:0;accent-color:var(–grn)} .ro.sel{border-color:var(–grn);background:var(–grn-lt)} /* ── Responsive ── */ @media(max-width:600px){ .diff-grid{grid-template-columns:1fr} nav{order:3;width:100%} .nb{flex:1;justify-content:center;font-size:.78rem} .fd{font-size:1.1rem} .prog-sum{grid-template-columns:repeat(3,1fr)} .sk-name{min-width:120px} }
    Moderne Wiskunde · Par. 9.2 · Lineaire formules vereenvoudigen · Offline beschikbaar
    /* ═══════════════════════════════════════════════════════ HELPERS ═══════════════════════════════════════════════════════ */ const ri = (a,b) => Math.floor(Math.random()*(b-a+1))+a; const ch = a => a[Math.floor(Math.random()*a.length)]; const esc = s => String(s).replace(/&/g,’&’).replace(//g,’>’).replace(/”/g,’"’); /* ═══════════════════════════════════════════════════════ GENERATOR ═══════════════════════════════════════════════════════ */ function termStr(val, isX, first) { const abs = Math.abs(val); if (isX) { const c = abs === 1 ? ” : abs; if (first) return val < 0 ? `-${c}x` : `${c}x`; return val < 0 ? ` – ${c}x` : ` + ${c}x`; } if (first) return String(val); return val termStr(t.v, t.x, i===0)).join(”); } function formatSimp(c, x) { if (!x && !c) return ‘y = 0′; const ax = Math.abs(x), xs = ax===1?’x’:`${ax}x`; if (!x) return `y = ${c}`; if (!c) return `y = ${x<0?'-':''}${xs}`; // Variabele eerst, daarna het getal const xPart = (x<0?'-':'') + xs; const cPart = c<0 ? `- ${Math.abs(c)}` : `+ ${c}`; return `y = ${xPart} ${cPart}`; } function genTerms(diff) { const sg = [-1,1]; if (diff === 'basis') { const xa=ri(1,7), xb=ri(1,7); const hc = Math.random()<.65; if (!hc) return [{v:xa,x:true},{v:xb,x:true}]; const cv=ri(2,15); return Math.random()<.5 ? [{v:cv,x:false},{v:xa,x:true},{v:xb,x:true}] : [{v:xa,x:true},{v:xb,x:true},{v:cv,x:false}]; } if (diff === 'normaal') { const xa=ri(1,9)*ch(sg); let xb=ri(1,9)*ch(sg); while(xa+xb===0) xb=ri(1,9)*ch(sg); const c1=ri(1,15), nc=ri(1,2); if (nc===1) return Math.random()<.5 ? [{v:c1,x:false},{v:xa,x:true},{v:xb,x:true}] : [{v:xa,x:true},{v:xb,x:true},{v:c1,x:false}]; const c2=ri(1,10)*ch(sg); return Math.random()<.5 ? [{v:c1,x:false},{v:xa,x:true},{v:xb,x:true},{v:c2,x:false}] : [{v:xa,x:true},{v:c1,x:false},{v:xb,x:true},{v:c2,x:false}]; } // uitdaging const nx=ri(2,3), nc=ri(1,Math.min(3,6-nx)); const xcs=[]; for(let i=0;is+v,0)+c===0); xcs.push(c); } const cs=Array.from({length:nc},()=>ri(1,20)*ch(sg)); const all=[…xcs.map(v=>({v,x:true})),…cs.map(v=>({v,x:false}))]; for(let i=all.length-1;i>0;i–){const j=ri(0,i);[all[i],all[j]]=[all[j],all[i]];} return all; } function isValid(terms){ const nx=terms.filter(t=>t.x).length; const sx=terms.filter(t=>t.x).reduce((s,t)=>s+t.v,0); return nx>=2 && sx!==0 && terms.length<=6; } function xlbl(v){const a=Math.abs(v);return(vt.x), ct=terms.filter(t=>!t.x); const h1=(ct.length ?`Zoek de gelijksoortige termen. De x-termen zijn: ${xt.map(t=>xlbl(t.v)).join(‘ en ‘)}. De getallen zijn: ${ct.map(t=>t.v).join(‘ en ‘)}. Alleen gelijksoortige termen kun je samenvoegen.` :`Zoek de gelijksoortige termen. De x-termen zijn: ${xt.map(t=>xlbl(t.v)).join(‘ en ‘)}. Er zijn geen losse getallen.`); const xc=xt.map(t=>t.v).join(‘ + ‘); const cc=ct.length?ct.map(t=>t.v).join(‘ + ‘):null; const h2=`Voeg de x-termen samen: (${xc}) = ${sx} → ${xlbl(sx)}. `+(cc?`Voeg de getallen samen: (${cc}) = ${sc}.`:`Er zijn geen losse getallen.`); const ax=Math.abs(sx), xp=(sx<0?'-':'')+(ax===1?'x':`${ax}x`); let h3=`De vereenvoudigde formule heeft de vorm y = `; if(sc&&sx){const cs=sct.x),ct=terms.filter(t=>!t.x); let s=`Gegeven: ${formatList(terms)}\n\n`; s+=`Stap 1 – Identificeer gelijksoortige termen:\n`; s+=` • x-termen: ${xt.map(t=>xlbl(t.v)).join(‘, ‘)}\n`; if(ct.length) s+=` • getallen: ${ct.map(t=>t.v).join(‘, ‘)}\n`; s+=`\nStap 2 – Voeg gelijksoortige termen samen:\n`; s+=` • x-termen: (${xt.map(t=>t.v).join(‘ + ‘)}) = ${sx} → ${xlbl(sx)}\n`; if(ct.length) s+=` • getallen: (${ct.map(t=>t.v).join(‘ + ‘)}) = ${sc}\n`; s+=`\nStap 3 – Vereenvoudigde formule:\n ${formatSimp(sc,sx)}`; return s; } function skillTags(diff,terms){ const t=[‘gelijksoortige-termen’]; if(terms.some(t=>t.x&&t.vt.x).length>2) t.push(‘meerdere-x-termen’); if(terms.filter(t=>!t.x).length>1) t.push(‘constanten-samenvoegen’); if(diff===’uitdaging’) t.push(‘grote-getallen’); return t; } function assert(cond,msg){if(!cond)console.warn(‘[Gen]’,msg);} function genExercise(diff, idx){ let terms=[]; let att=0; do{terms=genTerms(diff);att++;}while(!isValid(terms)&&attt.x).reduce((s,t)=>s+t.v,0); const sc=terms.filter(t=>!t.x).reduce((s,t)=>s+t.v,0); assert(sx!==0,’xCoeff should not be 0′); assert(terms.lengthc+x*v; /* ═══════════════════════════════════════════════════════ STORAGE ═══════════════════════════════════════════════════════ */ const PROG_KEY=’leerapp_lin_prog’, CUST_KEY=’leerapp_lin_cust’; function loadProg(){ try{const r=localStorage.getItem(PROG_KEY);if(r)return JSON.parse(r);}catch{} return{score:0,total:0,streak:0,best:0,skills:{},hist:[],sinceRefl:0}; } function saveProg(p){try{localStorage.setItem(PROG_KEY,JSON.stringify(p));}catch{}} function loadCust(){try{const r=localStorage.getItem(CUST_KEY);if(r)return JSON.parse(r);}catch{}return[];} function saveCust(q){try{localStorage.setItem(CUST_KEY,JSON.stringify(q));}catch{}} function dlJSON(data,name){ const b=new Blob([JSON.stringify(data,null,2)],{type:’application/json’}); const u=URL.createObjectURL(b),a=document.createElement(‘a’); a.href=u;a.download=name;document.body.appendChild(a);a.click(); document.body.removeChild(a);URL.revokeObjectURL(u); } /* ═══════════════════════════════════════════════════════ STATE ═══════════════════════════════════════════════════════ */ const S = { view:’home’, diff:’normaal’, ex:null, exIdx:0, phase:’answering’, // answering|wrong|correct|substitution|done hintsRev:0, showFull:false, ansVal:”, diag:”, orderWarn:”, reported:false, weOpen:true, subStep:’original’, subIn1:”, subIn2:”, subErr1:”, subErr2:”, prog:loadProg(), cust:loadCust(), mkForm:{q:”,a:”,h1:”,h2:”,h3:”,sol:”}, mkErr:”, mkOk:false, reflDiff:”, reflPlan:”, makerAuth:false, teacherAuth:false, authTarget:”, authErr:”, authVal:”, }; function setState(changes){ Object.assign(S,changes); render(); } /* ═══════════════════════════════════════════════════════ VIEW BUILDERS ═══════════════════════════════════════════════════════ */ // ── Header ── function buildHeader(){ const p=S.prog; const sc=p.total>0?`${p.score}/${p.total}`:”; const str=p.streak>=2?`
    🔥 ${p.streak}
    `:”; const nb=(v,label,extra=”)=>``; return` ${str}`; } // ── Home ── function buildHome(){ const p=S.prog; const statsHtml=p.total>0?`
    ${p.score}correct
    ${p.total>0?Math.round(p.score/p.total*100):0}%score
    ${p.streak}reeks
    ${p.best}beste
    `:”; const dlv=(d,lbl,dsc,actCls)=>``; return`
    📐

    Lineaire Formules Vereenvoudigen

    Par. 9.2 · Moderne Wiskunde · 1 havo/vwo

    Oefen met het samenvoegen van gelijksoortige termen.
    Denk aan: y = 12 − 4x − 2x → y = 12 − 6x

    ${statsHtml}

    Kies moeilijkheidsgraad

    ${dlv(‘basis’,’Basis’,’Alleen positieve getallen, 2–3 termen’,’on-b’)} ${dlv(‘normaal’,’Normaal’,’Mix van + en −, 3–4 termen’,’on-n’)} ${dlv(‘uitdaging’,’Uitdaging’,’Meer termen, grotere getallen, 4–6 termen’,’on-u’)}

    Hoe werkt het?

    • Vereenvoudig de formule en typ je antwoord (bijv. y = 12 - 6x)
    • Gebruik de Hint-knop als je vastloopt (max. 3 hints)
    • Bij elke 3e opgave: verplichte invulcontrole met x = 2
    • Na 5 opgaven: korte reflectievraag
    `; } // ── Worked example ── function buildWE(){ if(!S.weOpen) return`
    `; return`

    y = 12 − 4x − 2x

    Stap 1
    Identificeer gelijksoortige termen
    • x-termen: −4x en −2x
    • Getallen: 12
    Stap 2
    Voeg de x-termen samen

    (−4) + (−2) = −6 → −6x  ·  Het getal 12 blijft staan

    Stap 3
    Schrijf de vereenvoudigde formule

    y = −6x + 12

    ✎ Schrijf je antwoord als y = -6x + 12 — zet altijd eerst de variabele met de hoogste macht, daarna het getal.

    `; } // ── Hint panel ── function buildHints(){ if(!S.hintsRev && !S.showFull) return ”; const e=S.ex; let h=`
    `; if(S.hintsRev>0) h+=`
    Hints
    `; for(let i=0;i<S.hintsRev;i++){ h+=`
    Hint ${i+1}

    ${esc(e.hints[i])}

    `; } if(S.showFull){ h+=`

    Volledige uitwerking

    ${esc(e.fullSol)}
    `; } h+=`
    `; return h; } // ── Substitution check ── function buildSub(){ const e=S.ex, xv=2; const ey=evalF(e.sc,e.sx,xv); if(S.subStep===’done’){ return`
    ✓✓
    Super gecheckt!

    Beide formules geven y = ${ey} voor x = ${xv}. Jouw vereenvoudiging is bevestigd!

    `; } let html=`
    Controleer door in te vullen!

    Neem x = ${xv} en bereken y in beide formules.

    `; // Step A const aDone=S.subStep===’simplified’; html+=`
    Stap A – Originele formule

    ${esc(e.formula)}

    `; if(aDone){ html+=`

    ✓ y = ${ey}

    `; } else { html+=`
    ${S.subErr1?`

    ${esc(S.subErr1)}

    `:”}`; } html+=`
    `; // Step B if(S.subStep===’simplified’){ html+=`
    Stap B – Vereenvoudigde formule

    ${esc(e.simplified)}

    ${S.subErr2?`

    ${esc(S.subErr2)}

    `:”}
    `; } html+=`
    `; return html; } // ── Exercise card ── function buildExCard(){ if(!S.ex) return ”; const e=S.ex; const dbl={‘basis’:’db-b’,’normaal’:’db-n’,’uitdaging’:’db-u’}; const dlbl={‘basis’:’Basis’,’normaal’:’Normaal’,’uitdaging’:’Uitdaging’}; const withHelp=S.hintsRev>0||S.showFull; const isAns=S.phase===’answering’||S.phase===’wrong’; let html=`
    Opgave ${S.exIdx+1} ${dlbl[e.diff]} ${withHelp&&S.phase!==’answering’?’met hulp‘:”}
    ${e.needsSub?’📋 incl. invul-check‘:”}

    Vereenvoudig:

    ${esc(e.formula)}

    `; // Input row if(isAns){ html+=`
    `; } // Hints html+=buildHints(); // Feedback wrong if(S.phase===’wrong’){ html+=`
    Niet helemaal goed. ${S.diag?`

    ${esc(S.diag)}

    `:”} ${S.orderWarn?`

    ⚠ ${esc(S.orderWarn)}

    `:”} ${!S.showFull?`
    `:”}
    `; } // Substitution check if(S.phase===’substitution’) html+=buildSub(); // Feedback correct if(S.phase===’correct’){ html+=`
    Correct!${withHelp?’ (met hulp)‘:”}

    Antwoord: ${esc(e.simplified)}

    `; } // Done (saw solution) if(S.phase===’done’){ html+=`
    `; } html+=`
    `; return html; } // ── Practice view ── function buildPractice(){ return buildWE()+buildExCard(); } // ── Progress ── const SKILL_LBL={ ‘gelijksoortige-termen’:’Gelijksoortige termen’, ‘negatieve-coefficienten’:’Negatieve coëfficiënten’, ‘meerdere-x-termen’:’Meerdere x-termen’, ‘constanten-samenvoegen’:’Constanten samenvoegen’, ‘grote-getallen’:’Grote getallen’, }; function buildProgress(){ const p=S.prog; const acc=p.total>0?Math.round(p.score/p.total*100):0; let html=`

    Mijn voortgang

    ${p.score}Correct
    ${p.total}Totaal
    ${acc}%Score
    ${p.streak}Reeks nu
    ${p.best}Beste reeks
    `; const sk=Object.entries(p.skills); if(sk.length){ html+=`

    Per vaardigheid

    `; for(const[tag,stat]of sk){ const pct=stat.att>0?Math.round(stat.ok/stat.att*100):0; const cls=pct>=80?’sk-good’:pct>=50?’sk-ok’:’sk-low’; html+=`
    ${SKILL_LBL[tag]||tag}
    ${pct}% (${stat.ok}/${stat.att})
    `; } html+=`
    `; } const rec=[…p.hist].reverse().slice(0,10); if(rec.length){ html+=`

    Recente opgaven

    `; for(const a of rec){ const t=new Date(a.ts).toLocaleTimeString(‘nl-NL’,{hour:’2-digit’,minute:’2-digit’}); html+=`
    ${a.ok?’✓’:’✗’} ${a.diff} ${a.help?’met hulp‘:”} ${t}
    `; } html+=`
    `; } if(!p.total) html+=`

    Nog geen opgaven gemaakt. Ga oefenen om je voortgang te zien!

    `; if(p.total) html+=``; html+=`
    `; return html; } // ── Maker ── function buildMaker(){ const f=S.mkForm; return`

    Vraagmaker

    Maak zelf een opgave. De vraag wordt opgeslagen en is zichtbaar in de Docentmodus.

    ${S.cust.length?`

    Je hebt al ${S.cust.length} eigen ${S.cust.length===1?’vraag’:’vragen’} opgeslagen.

    `:”}
    ${S.mkErr?`

    ${esc(S.mkErr)}

    `:”} ${S.mkOk?’

    ✓ Vraag opgeslagen!

    ‘:”}
    `; } // ── Auth ── function buildAuth(){ const label=S.authTarget===’maker’?’Vraagmaker’:’Docentmodus’; return`

    🔒 ${label}

    Voer het wachtwoord in om toegang te krijgen tot de ${label}.

    ${S.authErr?`

    ${esc(S.authErr)}

    `:”}
    `; } // ── Teacher ── function buildTeacher(){ const p=S.prog; const acc=p.total>0?Math.round(p.score/p.total*100)+’%’:’—’; let html=`

    Docentmodus

    Docent

    Opgeslagen vragen (${S.cust.length})

    `; if(!S.cust.length){ html+=`

    Nog geen eigen vragen. Gebruik de Vraagmaker om vragen toe te voegen.

    `; } else { for(const q of S.cust){ const d=new Date(q.ts).toLocaleDateString(‘nl-NL’,{day:’2-digit’,month:’2-digit’,year:’numeric’}); html+=`
    ${esc(q.q)}→ ${esc(q.a)}
    ${q.hints.map((h,i)=>`Hint ${i+1}: ${esc(h)}`).join(”)}
    Aangemaakt: ${d}
    `; } } html+=`

    Voortgangsoverzicht

    Totaal gemaakt${p.total}
    Correct${p.score}
    Score${acc}
    Beste reeks${p.best}

    Alle data is opgeslagen in de browser. Exporteer als JSON om op te slaan of te delen.

    `; return html; } // ── Reflection ── function buildRefl(){ const opts=[‘Het herkennen van gelijksoortige termen’,’Werken met negatieve getallen’,’Meerdere x-termen samenvoegen’,’Meerdere getallen samenvoegen’,’De formule opschrijven’,’Niets — het ging goed!’]; let html=`
    🌿

    Even nadenken…

    Je hebt 5 opgaven gemaakt. Neem even een moment.

    `; for(const o of opts){ html+=``; } html+=`
    `; return html; } /* ═══════════════════════════════════════════════════════ RENDER ═══════════════════════════════════════════════════════ */ function render(){ document.getElementById(‘hdr’).innerHTML=buildHeader(); let content=”; switch(S.view){ case ‘home’: content=buildHome(); break; case ‘practice’: content=buildPractice(); break; case ‘progress’: content=buildProgress(); break; case ‘maker’: content=buildMaker(); break; case ’teacher’: content=buildTeacher(); break; case ‘auth’: content=buildAuth(); break; } document.getElementById(‘main’).innerHTML=content; // Focus answer input if in answering phase if(S.view===’practice’&&(S.phase===’answering’||S.phase===’wrong’)){ const inp=document.getElementById(‘ans-inp’); if(inp){inp.focus();inp.setSelectionRange(inp.value.length,inp.value.length);} } // Focus auth input if(S.view===’auth’){const e=document.getElementById(‘auth-inp’);if(e)e.focus();} // Focus sub inputs if(S.phase===’substitution’){ if(S.subStep===’original’){const e=document.getElementById(‘sub1′);if(e)e.focus();} if(S.subStep===’simplified’){const e=document.getElementById(‘sub2’);if(e)e.focus();} } renderRefl(); } function renderRefl(){ const el=document.getElementById(‘refl-overlay’); if(S.showRefl){el.style.display=’flex’;el.innerHTML=buildRefl();} else{el.style.display=’none’;el.innerHTML=”;} } /* ═══════════════════════════════════════════════════════ EVENT HANDLERS (global) ═══════════════════════════════════════════════════════ */ function navTo(v){ if((v===’maker’&&!S.makerAuth)||(v===’teacher’&&!S.teacherAuth)){ S.authTarget=v; S.view=’auth’; S.authErr=”; S.authVal=”; } else { S.view=v; } render(); } function submitAuth(){ if(S.authVal===’Bussum2025′){ if(S.authTarget===’maker’) S.makerAuth=true; if(S.authTarget===’teacher’) S.teacherAuth=true; S.view=S.authTarget; S.authErr=”; S.authVal=”; } else { S.authErr=’Onjuist wachtwoord. Probeer opnieuw.’; } render(); } function setDiff(d){ S.diff=d; render(); } function startPractice(){ const e=genExercise(S.diff,S.exIdx); S.ex=e; S.view=’practice’; S.phase=’answering’; S.hintsRev=0; S.showFull=false; S.ansVal=”; S.diag=”; S.reported=false; S.weOpen=true; S.subStep=’original’; S.subIn1=”; S.subIn2=”; S.subErr1=”; S.subErr2=”; render(); } function toggleWE(){S.weOpen=!S.weOpen;render();} function hasConstantFirst(raw){ const expr=raw.trim().replace(/^y\s*=\s*/i,”).trim(); return /^-?\s*\d+(?:\.\d+)?\s*(?![x])/i.test(expr); } function checkBtn(){ const p=parseAns(S.ansVal); if(!p){ S.diag=’Ik begrijp je antwoord niet. Schrijf het als: y = -6x + 12′; S.orderWarn=”; S.phase=’wrong’; render(); return; } const ok=p.c===S.ex.sc&&p.x===S.ex.sx; if(ok){ S.diag=”; S.orderWarn=”; report(true); S.phase=S.ex.needsSub?’substitution’:’correct’; S.subStep=’original’; S.subIn1=”; S.subIn2=”; S.subErr1=”; S.subErr2=”; } else { S.diag=diagErr(p,S.ex.sc,S.ex.sx); S.orderWarn=hasConstantFirst(S.ansVal)? ‘Let op: zet altijd eerst de variabele met de hoogste macht en daarna het getal.’:”; S.phase=’wrong’; } render(); } function hintBtn(){if(S.hintsRev0||S.showFull; p.total++; if(ok){p.score++;p.streak++;if(p.streak>p.best)p.best=p.streak;} else{p.streak=0;} p.sinceRefl++; p.hist.push({diff:S.diff,ok,help,hintsUsed:S.hintsRev,ts:Date.now()}); if(S.ex){ for(const tag of S.ex.tags){ if(!p.skills[tag])p.skills[tag]={att:0,ok:0}; p.skills[tag].att++; if(ok)p.skills[tag].ok++; } } saveProg(p); if(p.sinceRefl>=5){p.sinceRefl=0;saveProg(p);S.showRefl=true;} } function nextEx(){ S.exIdx++; const e=genExercise(S.diff,S.exIdx); S.ex=e; S.phase=’answering’; S.hintsRev=0; S.showFull=false; S.ansVal=”; S.diag=”; S.reported=false; S.subStep=’original’; S.subIn1=”; S.subIn2=”; S.subErr1=”; S.subErr2=”; render(); } function checkSub1(){ const xv=2, ey=evalF(S.ex.sc,S.ex.sx,xv); const val=parseFloat(S.subIn1); if(isNaN(val)){S.subErr1=’Vul een getal in.’;render();return;} if(val!==ey){S.subErr1=`Dat klopt nog niet. Vul x = ${xv} in in elke term van de originele formule.`;render();return;} S.subErr1=”;S.subStep=’simplified’;render(); } function checkSub2(){ const xv=2, ey=evalF(S.ex.sc,S.ex.sx,xv); const val=parseFloat(S.subIn2); if(isNaN(val)){S.subErr2=’Vul een getal in.’;render();return;} if(val!==ey){S.subErr2=`Dat klopt nog niet. Vul x = ${xv} in in de vereenvoudigde formule.`;render();return;} S.subErr2=”;S.subStep=’done’;render(); // After sub done, mark as correct (next button is inside sub-done) // nextEx is called by clicking the button in sub-done } function doneRefl(){ S.showRefl=false; S.reflDiff=”; S.reflPlan=”; nextEx(); } function saveMaker(){ const f=S.mkForm; if(!f.q.trim()){S.mkErr=’Voer een opgave in.’;S.mkOk=false;render();return;} if(!f.a.trim()){S.mkErr=’Voer het juiste antwoord in.’;S.mkOk=false;render();return;} if(!f.h1.trim()||!f.h2.trim()||!f.h3.trim()){S.mkErr=’Alle 3 hints zijn verplicht.’;S.mkOk=false;render();return;} if(!f.sol.trim()){S.mkErr=’Voer een volledige uitwerking in.’;S.mkOk=false;render();return;} const q={id:`c-${Date.now()}`,q:f.q.trim(),a:f.a.trim(),hints:[f.h1.trim(),f.h2.trim(),f.h3.trim()],sol:f.sol.trim(),ts:Date.now()}; S.cust.push(q);saveCust(S.cust); S.mkForm={q:”,a:”,h1:”,h2:”,h3:”,sol:”}; S.mkErr=”;S.mkOk=true; render(); } function delQ(id){ if(!confirm(‘Vraag verwijderen?’))return; S.cust=S.cust.filter(q=>q.id!==id);saveCust(S.cust);render(); } function expQ(){dlJSON(S.cust,`vraagbank-lineair-${new Date().toISOString().slice(0,10)}.json`);} function expP(){dlJSON({exportedAt:new Date().toISOString(),progress:S.prog},`voortgang-lineair-${new Date().toISOString().slice(0,10)}.json`);} function doReset(){ if(!confirm(‘Wil je alle voortgang wissen?’))return; S.prog={score:0,total:0,streak:0,best:0,skills:{},hist:[],sinceRefl:0}; saveProg(S.prog);render(); } /* ═══════════════════════════════════════════════════════ INIT ═══════════════════════════════════════════════════════ */ render();
  • Lineaire Formules — 1 havo/vwo :root { –bg: #f5f1e8; –surface: #fefcf8; –surface2: #ece8de; –border: #d8d2c6; –border-light: #e8e4dc; –primary: #5b7c5e; –primary-dk: #3d5c40; –primary-lt: #e6f0e8; –accent: #b5783e; –accent-lt: #f5e8d4; –text: #2a2520; –text-muted: #6e6860; –ok: #3a7a50; –ok-bg: #e6f5ec; –err: #a83838; –err-bg: #fae6e6; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: ‘Georgia’, ‘Times New Roman’, serif; background: var(–bg); color: var(–text); min-height: 100vh; line-height: 1.6; } .wrap { max-width: 760px; margin: 0 auto; padding: 20px 16px 60px; } /* ── HEADER ───────────────────────────────── */ .app-header { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 14px; border-bottom: 1px solid var(–border); margin-bottom: 18px; gap: 10px; flex-wrap: wrap; } .app-title { font-size: 1.1rem; font-weight: normal; color: var(–primary-dk); } .app-sub { font-size: 0.78rem; color: var(–text-muted); margin-top: 2px; } .header-nav { display: flex; gap: 7px; flex-wrap: wrap; } /* ── BUTTONS ──────────────────────────────── */ .btn { display: inline-block; padding: 8px 15px; border: none; border-radius: 7px; cursor: pointer; font-family: inherit; font-size: 0.87rem; transition: background 0.13s, transform 0.08s; white-space: nowrap; line-height: 1.4; } .btn:active { transform: scale(0.97); } .btn-primary { background: var(–primary); color: #fff; } .btn-primary:hover { background: var(–primary-dk); } .btn-secondary { background: var(–surface); color: var(–text); border: 1px solid var(–border); } .btn-secondary:hover { background: var(–surface2); } .btn-hint { background: var(–accent-lt); color: var(–accent); border: 1px solid #d4b88a; } .btn-hint:hover { background: #f0ddc4; } .btn-ghost { background: transparent; color: var(–text-muted); border: 1px dashed var(–border); font-size: 0.81rem; } .btn-ghost:hover { background: var(–surface2); } .btn-big { padding: 12px 30px; font-size: 1rem; } /* ── CARDS ────────────────────────────────── */ .card { background: var(–surface); border-radius: 10px; border: 1px solid var(–border-light); box-shadow: 0 1px 5px rgba(0,0,0,0.06); padding: 22px; margin-bottom: 14px; } /* ── PROGRESS ─────────────────────────────── */ .prog-wrap { margin-bottom: 13px; } .prog-labels { display: flex; justify-content: space-between; font-size: 0.79rem; color: var(–text-muted); margin-bottom: 5px; } .prog-track { height: 5px; background: var(–surface2); border-radius: 3px; overflow: hidden; } .prog-fill { height: 100%; background: var(–primary); border-radius: 3px; transition: width 0.5s ease; } /* ── STATS ROW ────────────────────────────── */ .stats-row { display: flex; gap: 9px; margin-bottom: 14px; } .stat-chip { flex: 1; background: var(–surface); border: 1px solid var(–border); border-radius: 8px; padding: 10px 6px; text-align: center; } .stat-num { font-size: 1.3rem; font-weight: bold; color: var(–primary-dk); line-height: 1; } .stat-lbl { font-size: 0.7rem; color: var(–text-muted); margin-top: 2px; } /* ── WORKED EXAMPLE ───────────────────────── */ .example-wrap { border-radius: 10px; border: 1px solid #c2d8c6; background: var(–primary-lt); margin-bottom: 14px; overflow: hidden; } .example-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; cursor: pointer; user-select: none; } .example-head-title { font-size: 0.83rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(–primary-dk); } .example-chevron { color: var(–primary); font-size: 0.9rem; } .example-body { padding: 4px 20px 18px; border-top: 1px solid #c2d8c6; } .example-body ol { padding-left: 22px; line-height: 2.1; margin: 8px 0; } .example-body .ans { margin-top: 8px; font-size: 1.05rem; } .divider { border: none; border-top: 1px solid var(–border); margin: 14px 0; } /* ── EXERCISE CARD ────────────────────────── */ .ex-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } .skill-pill { font-size: 0.7rem; background: var(–surface2); color: var(–text-muted); border: 1px solid var(–border); border-radius: 20px; padding: 2px 10px; } .help-badge { font-size: 0.68rem; background: var(–accent-lt); color: var(–accent); border: 1px solid #d4b88a; border-radius: 20px; padding: 2px 8px; } .diff-pip { color: var(–text-muted); font-size: 0.68rem; letter-spacing: 1px; } .ex-formula { font-size: 1.55rem; color: var(–primary-dk); font-style: italic; margin-bottom: 5px; } .ex-question { font-size: 1rem; color: var(–text); margin-bottom: 18px; } /* ── INPUT ROW ────────────────────────────── */ .input-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 13px; } .num-input { width: 88px; padding: 9px 11px; font-size: 1.1rem; font-family: inherit; border: 2px solid var(–border); border-radius: 7px; background: var(–surface); color: var(–text); text-align: center; transition: border-color 0.13s; } .num-input:focus { outline: none; border-color: var(–primary); } .num-input.ok { border-color: var(–ok); background: var(–ok-bg); } .num-input.wrong { border-color: var(–err); background: var(–err-bg); animation: shake 0.28s; } @keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } .solved-label { font-size: 0.95rem; color: var(–ok); font-weight: bold; } /* ── FEEDBACK ─────────────────────────────── */ .feedback { padding: 11px 15px; border-radius: 7px; font-size: 0.91rem; margin-bottom: 12px; } .feedback.ok { background: var(–ok-bg); color: var(–ok); border: 1px solid #a8d4b8; } .feedback.err { background: var(–err-bg); color: var(–err); border: 1px solid #e8b4b4; } /* ── HINTS ────────────────────────────────── */ .hints-wrap { margin-top: 10px; } .hint-box { padding: 10px 14px; background: #f8f3e8; border-left: 3px solid var(–accent); margin-bottom: 8px; border-radius: 0 7px 7px 0; } .hint-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(–accent); margin-bottom: 3px; font-weight: bold; } .hint-text { font-size: 0.91rem; } /* ── FULL SOLUTION ────────────────────────── */ .solution-box { background: var(–surface2); border: 1px solid var(–border); border-radius: 8px; padding: 16px; margin-top: 10px; } .solution-title { font-size: 0.73rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(–text-muted); margin-bottom: 10px; } .solution-step { font-size: 0.93rem; font-family: ‘Courier New’, monospace; padding: 2px 0; color: var(–text); } /* ── REFLECTION ───────────────────────────── */ .refl-card { background: linear-gradient(145deg, #f5ede0, #e8f0e9); border: 1px solid #d4c4a8; border-radius: 10px; padding: 22px; margin-bottom: 14px; } .refl-title { font-size: 1rem; color: var(–accent); margin-bottom: 10px; } .refl-qs { font-size: 0.91rem; line-height: 1.9; margin-bottom: 12px; } .refl-ta { width: 100%; min-height: 90px; padding: 10px; font-family: inherit; font-size: 0.88rem; border: 1px solid var(–border); border-radius: 6px; background: rgba(255,255,255,0.75); resize: vertical; color: var(–text); } /* ── SKILL TABLE ──────────────────────────── */ .skill-card h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(–text-muted); margin-bottom: 12px; font-weight: normal; } .skill-table { width: 100%; border-collapse: collapse; font-size: 0.87rem; } .skill-table th { text-align: left; padding: 6px 10px; color: var(–text-muted); font-weight: normal; border-bottom: 1px solid var(–border); font-size: 0.78rem; } .skill-table td { padding: 8px 10px; border-bottom: 1px solid var(–border-light); } .skill-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; } /* ── GRADE SCREEN ─────────────────────────── */ .grade-screen { text-align: center; padding: 18px 10px; } .grade-lbl { font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(–text-muted); } .grade-num { font-size: 9rem; font-weight: bold; line-height: 1.05; margin: 6px 0; } .grade-num.excellent { color: #2d7a4a; } .grade-num.good { color: var(–primary); } .grade-num.ok-grade { color: var(–accent); } .grade-num.poor { color: var(–err); } .grade-msg { font-size: 1.25rem; margin-bottom: 18px; } .grade-info { background: var(–surface2); border-radius: 8px; padding: 15px 18px; margin: 14px 0; text-align: left; } .grade-info p { font-size: 0.9rem; margin-bottom: 4px; } .grade-info .formula { font-size: 0.8rem; color: var(–text-muted); font-family: monospace; margin-top: 6px; } /* ── MAKER / TEACHER ──────────────────────── */ .page-title { font-size: 1.05rem; margin-bottom: 16px; color: var(–primary-dk); } .form-row { margin-bottom: 13px; } .form-lbl { display: block; font-size: 0.79rem; color: var(–text-muted); margin-bottom: 4px; } .form-input { width: 100%; padding: 8px 11px; font-family: inherit; font-size: 0.88rem; border: 1px solid var(–border); border-radius: 6px; background: var(–surface); color: var(–text); } .form-textarea { width: 100%; padding: 8px 11px; font-family: inherit; font-size: 0.88rem; border: 1px solid var(–border); border-radius: 6px; background: var(–surface); resize: vertical; min-height: 64px; color: var(–text); } .custom-item { padding: 10px 14px; background: var(–surface2); border-radius: 6px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; gap: 10px; } .custom-item .ci-formula { font-style: italic; font-size: 0.93rem; } .custom-item .ci-question { font-size: 0.78rem; color: var(–text-muted); margin-top: 2px; } /* ── ANIMATIONS ───────────────────────────── */ @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .fade-up { animation: fadeUp 0.22s ease; } /* ── RESPONSIVE ───────────────────────────── */ @media (max-width: 500px) { .ex-formula { font-size: 1.2rem; } .grade-num { font-size: 5.5rem; } .stats-row { gap: 6px; } }
    ‘use strict’; /* ═══════════════════════════════════════════════════════════════════════════ TYPE DEFINITIONS (JSDoc) ═══════════════════════════════════════════════════════════════════════════ */ /** * @typedef {‘lineair-eenvoudig’|’lineair-negatief’|’lineair-haakjes’} SkillTag * @typedef {1|2|3} Difficulty * * @typedef {Object} Exercise * @property {string} id * @property {string} formulaDisplay – e.g. “y = 3x + 5” * @property {string} questionText – e.g. “Bereken y als x = 4″ * @property {string} depVar – dependent variable * @property {string} indVar – independent variable * @property {number} indValue – substituted value * @property {number} answer * @property {string[]} hints – exactly 3 items * @property {string[]} solutionSteps – lines of worked solution * @property {SkillTag} skillTag * @property {Difficulty} difficulty * @property {boolean} [isCustom] * * @typedef {Object} Completed * @property {SkillTag} skillTag * @property {number} attempts * @property {boolean} usedHint * @property {number} points */ /* ═══════════════════════════════════════════════════════════════════════════ MATH HELPERS ═══════════════════════════════════════════════════════════════════════════ */ const rnd = (a, b) => Math.floor(Math.random() * (b – a + 1)) + a; const pick = arr => arr[Math.floor(Math.random() * arr.length)]; const nonZero = (a, b) => { let v; do { v = rnd(a, b); } while (v === 0); return v; }; const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6); /** * Format coefficient: 1→”, -1→’-‘, 3→’3′, -3→’-3′ * @param {number} n * @returns {string} */ function fmtCoeff(n) { if (n === 1) return ”; if (n === -1) return ‘-‘; return String(n); } /** * Format constant term suffix: 0→”, 5→’ + 5′, -3→’ – 3′ * @param {number} n * @returns {string} */ function fmtK(n) { if (n === 0) return ”; return n > 0 ? ` + ${n}` : ` – ${Math.abs(n)}`; } /* ═══════════════════════════════════════════════════════════════════════════ GENERATOR — Type 1: depVar = a·indVar + b ═══════════════════════════════════════════════════════════════════════════ */ /** * @param {Difficulty} diff * @returns {Exercise} */ function genLinear(diff) { const PAIRS = [[‘y’,’x’],[‘p’,’m’],[‘l’,’m’],[‘s’,’t’],[‘v’,’n’],[‘y’,’t’],[‘p’,’q’]]; const [dep, ind] = pick(PAIRS); let a, b, xv; if (diff === 1) { a = nonZero(2, 5); b = rnd(0, 10); xv = rnd(1, 7); } else if (diff === 2) { a = nonZero(-6, 6); b = rnd(-12, 12); xv = rnd(-4, 8); } else { a = nonZero(-8, 8); b = rnd(-15, 15); xv = rnd(-6, 10); } const step1 = a * xv; const answer = step1 + b; const formulaDisplay = `${dep} = ${fmtCoeff(a)}${ind}${fmtK(b)}`; const questionText = `Bereken ${dep} als ${ind} = ${xv}`; /* Hint 1 – substitution */ let subExpr; if (a === 1) subExpr = `${dep} = ${xv}${fmtK(b)}`; else if (a === -1) subExpr = `${dep} = -${xv}${fmtK(b)}`; else subExpr = `${dep} = ${a} × ${xv}${fmtK(b)}`; const hint1 = `Vul ${ind} = ${xv} in: ${subExpr}`; /* Hint 2 – multiplication */ const hint2 = Math.abs(a) === 1 ? `De factor voor ${ind} is ${a}, dus ${a} × ${xv} = ${step1}` : `Bereken de vermenigvuldiging: ${a} × ${xv} = ${step1}`; /* Hint 3 – addition */ const hint3 = b === 0 ? `Er is geen getal bij op te tellen: ${dep} = ${step1}` : `Bereken de optelling: ${step1}${fmtK(b)} = ${answer}`; /* Full solution steps */ const solutionSteps = b === 0 ? [`${dep} = ${a} × ${xv}`, ` = ${answer}`] : [`${dep} = ${a} × ${xv} + (${b})`, ` = ${step1} + (${b})`, ` = ${answer}`]; const skillTag = (a < 0 || b 0.35 ? rnd(-6, 6) : 0; const xv = rnd(-3, 5); const bracketVal = b * xv + c; // value inside brackets after substitution const beforeD = a * bracketVal; const answer = beforeD + d; /* Formula display */ let indTerm; if (b === 1) indTerm = ind; else if (b === -1) indTerm = `-${ind}`; else indTerm = `${b}${ind}`; const formulaDisplay = `${dep} = ${a}(${indTerm}${fmtK(c)})${fmtK(d)}`; const questionText = `Bereken ${dep} als ${ind} = ${xv}`; /* Hint 1 – substitution */ let subIndTerm; if (b === 1) subIndTerm = String(xv); else if (b === -1) subIndTerm = String(-xv); else subIndTerm = `${b} × ${xv}`; const hint1 = `Vul ${ind} = ${xv} in: ${dep} = ${a}(${subIndTerm}${fmtK(c)})${fmtK(d)}`; /* Hint 2 – bracket calculation */ const bracketStep = b * xv; let bracketDetail; if (b === 1) bracketDetail = `${xv}${fmtK(c)} = ${bracketVal}`; else if (b === -1) bracketDetail = `-${xv}${fmtK(c)} = ${bracketVal}`; else if (c === 0) bracketDetail = `${b} × ${xv} = ${bracketVal}`; else bracketDetail = `${b} × ${xv}${fmtK(c)} = ${bracketStep}${fmtK(c)} = ${bracketVal}`; const hint2 = `Bereken eerst de haakjes: ${bracketDetail}`; /* Hint 3 – final calculation */ const hint3 = d !== 0 ? `Bereken nu: ${a} × ${bracketVal}${fmtK(d)} = ${beforeD}${fmtK(d)} = ${answer}` : `Bereken nu: ${a} × ${bracketVal} = ${answer}`; /* Full solution steps */ const solutionSteps = d === 0 ? [ `${dep} = ${a} × (${b} × ${xv} + (${c}))`, ` = ${a} × ${bracketVal}`, ` = ${answer}` ] : [ `${dep} = ${a} × (${b} × ${xv} + (${c})) + (${d})`, ` = ${a} × ${bracketVal} + (${d})`, ` = ${beforeD} + (${d})`, ` = ${answer}` ]; return { id: uid(), formulaDisplay, questionText, depVar: dep, indVar: ind, indValue: xv, answer, hints: [hint1, hint2, hint3], solutionSteps, skillTag: ‘lineair-haakjes’, difficulty: 3 }; } /* ═══════════════════════════════════════════════════════════════════════════ ADAPTIVE EXERCISE SELECTOR ═══════════════════════════════════════════════════════════════════════════ */ /** * @param {Object} state * @returns {Exercise} */ function generateNext(state) { /* Occasionally surface a custom exercise */ if (state.customExercises.length > 0 && Math.random() 0 ? state.score / (state.completed.length * 2) : 1; /* Adaptive difficulty */ let diff; if (n < 3) diff = 1; else if (n < 7 || rate = 2 && n >= 5 && Math.random() < 0.38); return useBracket ? genBracket() : genLinear(diff); } /* ═══════════════════════════════════════════════════════════════════════════ RUNTIME ASSERTS (unit-test-like generator checks) ═══════════════════════════════════════════════════════════════════════════ */ function runChecks() { let ok = true; for (let i = 0; i < 60; i++) { const ex = genLinear(rnd(1, 3)); if (ex.hints.length !== 3) { console.error('FAIL genLinear hints', ex); ok = false; } if (!Number.isFinite(ex.answer)) { console.error('FAIL genLinear answer', ex); ok = false; } if (!ex.skillTag) { console.error('FAIL genLinear tag', ex); ok = false; } if (ex.solutionSteps.length < 2) { console.error('FAIL genLinear steps', ex); ok = false; } } for (let i = 0; i < 30; i++) { const ex = genBracket(); if (ex.hints.length !== 3) { console.error('FAIL genBracket hints', ex); ok = false; } if (ex.skillTag !== 'lineair-haakjes') { console.error('FAIL genBracket tag', ex); ok = false; } if (!Number.isFinite(ex.answer)) { console.error('FAIL genBracket answer', ex); ok = false; } if (ex.solutionSteps.length { try { return JSON.parse(localStorage.getItem(LS_KEY) || ‘[]’); } catch { return []; } }; const saveSaved = arr => localStorage.setItem(LS_KEY, JSON.stringify(arr)); /* ═══════════════════════════════════════════════════════════════════════════ APPLICATION STATE ═══════════════════════════════════════════════════════════════════════════ */ /** @type {Object} */ const S = { mode: ‘learn’, // ‘learn’ | ‘reflection’ | ‘grade’ | ‘maker’ | ’teacher’ ex: null, // current Exercise hintLevel: 0, // 0–3 showSolution: false, attempts: 0, solved: false, fb: null, // { text: string, ok: boolean } | null exerciseCount: 0, score: 0, streak: 0, completed: [], // Completed[] skillStats: { ‘lineair-eenvoudig’: { correct: 0, total: 0, withHelp: 0 }, ‘lineair-negatief’: { correct: 0, total: 0, withHelp: 0 }, ‘lineair-haakjes’: { correct: 0, total: 0, withHelp: 0 } }, workedExampleOpen: true, customExercises: loadSaved(), makerErr: null, reflectionText: ”, reflectionsDone: [] // which counts have had reflections (5, 10) }; /* ═══════════════════════════════════════════════════════════════════════════ RENDER SYSTEM ═══════════════════════════════════════════════════════════════════════════ */ function render() { const root = document.createElement(‘div’); root.className = ‘wrap’; root.id = ‘app’; root.appendChild(buildHeader()); switch (S.mode) { case ‘grade’: root.appendChild(buildGradeScreen()); break; case ‘reflection’: root.appendChild(buildProgBar()); root.appendChild(buildStats()); root.appendChild(buildReflection()); root.appendChild(buildSkillTable()); break; case ‘maker’: root.appendChild(buildMaker()); break; case ’teacher’: root.appendChild(buildTeacher()); break; default: root.appendChild(buildProgBar()); root.appendChild(buildStats()); root.appendChild(buildWorkedExample()); if (S.ex) root.appendChild(buildExCard()); } const old = document.getElementById(‘app’); old.parentNode.replaceChild(root, old); if (S.mode === ‘learn’ && S.ex && !S.solved) { setTimeout(() => { const inp = document.getElementById(‘answerInput’); if (inp) inp.focus(); }, 40); } } /* ── HEADER ──────────────────────────────────────────────────────────────── */ function buildHeader() { const el = document.createElement(‘div’); el.className = ‘app-header’; el.innerHTML = `
    Lineaire Formules
    1 havo/vwo — Moderne Wiskunde
    `; el.querySelector(‘#btnMaker’).addEventListener(‘click’, enterMaker); el.querySelector(‘#btnTeacher’).addEventListener(‘click’, enterTeacher); return el; } /* ── PROGRESS BAR ────────────────────────────────────────────────────────── */ function buildProgBar() { const pct = Math.min((S.exerciseCount / 15) * 100, 100); const el = document.createElement(‘div’); el.className = ‘prog-wrap’; el.innerHTML = `
    Voortgang${S.exerciseCount} / 15 opgaven
    `; return el; } /* ── STATS ROW ───────────────────────────────────────────────────────────── */ function buildStats() { const icon = S.streak >= 5 ? ‘🔥’ : S.streak >= 3 ? ‘⭐’ : ‘–’; const el = document.createElement(‘div’); el.className = ‘stats-row’; el.innerHTML = `
    ${S.score}
    Punten
    ${icon} ${S.streak}
    Streak
    ${S.exerciseCount}
    Gemaakt
    `; return el; } /* ── WORKED EXAMPLE ──────────────────────────────────────────────────────── */ function buildWorkedExample() { const wrap = document.createElement(‘div’); wrap.className = ‘example-wrap’; const head = document.createElement(‘div’); head.className = ‘example-head’; head.innerHTML = ` 📖 Uitgewerkt voorbeeld — bekijk dit eerst ${S.workedExampleOpen ? ‘▲’ : ‘▼’}`; head.addEventListener(‘click’, () => { S.workedExampleOpen = !S.workedExampleOpen; render(); }); wrap.appendChild(head); if (S.workedExampleOpen) { const body = document.createElement(‘div’); body.className = ‘example-body’; body.innerHTML = `

    Voorbeeld 1 — zonder haakjes: y = 2x + 3, gegeven x = 4

    1. Vul x = 4 in: y = 2 · 4 + 3
    2. Bereken de vermenigvuldiging: 2 · 4 = 8
    3. Tel op: 8 + 3 = 11

    Antwoord: y = 11


    Voorbeeld 2 — met haakjes: p = −3(q + 2) − 5, gegeven q = 3

    1. Vul q = 3 in: p = −3(3 + 2) − 5
    2. Bereken de haakjes: 3 + 2 = 5, dus p = −3 · 5 − 5
    3. Bereken: −3 · 5 = −15, dus p = −15 − 5 = −20

    Antwoord: p = −20

    `; wrap.appendChild(body); } return wrap; } /* ── EXERCISE CARD ───────────────────────────────────────────────────────── */ function buildExCard() { const ex = S.ex; const card = document.createElement(‘div’); card.className = ‘card fade-up’; /* Meta row */ const meta = document.createElement(‘div’); meta.className = ‘ex-meta’; meta.innerHTML = ` ${skillLabel(ex.skillTag)} ${S.hintLevel > 0 ? ‘met hulp‘ : ”} ${‘◆’.repeat(ex.difficulty)}${‘◇’.repeat(3 – ex.difficulty)}`; card.appendChild(meta); /* Formula & question */ const fDiv = document.createElement(‘div’); fDiv.className = ‘ex-formula’; fDiv.innerHTML = `${ex.formulaDisplay}`; card.appendChild(fDiv); const qDiv = document.createElement(‘div’); qDiv.className = ‘ex-question’; qDiv.textContent = ex.questionText; card.appendChild(qDiv); /* Input area */ if (!S.solved) { const row = document.createElement(‘div’); row.className = ‘input-row’; const inp = document.createElement(‘input’); inp.type = ‘number’; inp.id = ‘answerInput’; inp.className = ‘num-input’ + (S.fb && !S.fb.ok ? ‘ wrong’ : ”); inp.placeholder = ‘?’; inp.addEventListener(‘keydown’, e => { if (e.key === ‘Enter’) doCheck(); }); row.appendChild(inp); const checkBtn = document.createElement(‘button’); checkBtn.className = ‘btn btn-primary’; checkBtn.textContent = ‘Controleer’; checkBtn.addEventListener(‘click’, doCheck); row.appendChild(checkBtn); if (S.hintLevel 0) { const hintsWrap = document.createElement(‘div’); hintsWrap.className = ‘hints-wrap’; for (let i = 0; i < S.hintLevel; i++) { const hbox = document.createElement('div'); hbox.className = 'hint-box'; hbox.innerHTML = `
    Hint ${i + 1}
    ${ex.hints[i]}
    `; hintsWrap.appendChild(hbox); } card.appendChild(hintsWrap); } /* Full solution */ if (S.showSolution) { const solBox = document.createElement(‘div’); solBox.className = ‘solution-box’; solBox.innerHTML = `
    Volledige uitwerking
    `; ex.solutionSteps.forEach(line => { const p = document.createElement(‘div’); p.className = ‘solution-step’; p.textContent = line; solBox.appendChild(p); }); card.appendChild(solBox); } return card; } /* ── REFLECTION ──────────────────────────────────────────────────────────── */ function buildReflection() { const card = document.createElement(‘div’); card.className = ‘refl-card’; card.innerHTML = `
    🤔 Reflectiemoment — na ${S.exerciseCount} opgaven
    Je hebt nu ${S.exerciseCount} opgaven gemaakt. Neem even de tijd om na te denken:

    1. Wat was jouw aanpak? Welke stappen zette je?
    2. Welke stap vond je het lastigst, en waarom?
    `; card.querySelector(‘#continueBtn’).addEventListener(‘click’, doContinue); return card; } /* ── SKILL TABLE ─────────────────────────────────────────────────────────── */ function buildSkillTable() { const card = document.createElement(‘div’); card.className = ‘card skill-card’; const rows = Object.entries(S.skillStats).map(([tag, st]) => { const dot = ``; const pct = st.total > 0 ? Math.round((st.correct / st.total) * 100) + ‘%’ : ‘—’; return ` ${dot}${skillLabel(tag)} ${st.correct}${st.total}${st.withHelp}${pct} `; }).join(”); card.innerHTML = `

    Overzicht per vaardigheid

    ${rows}
    VaardigheidGoedTotaalMet hulp%
    `; return card; } /* ── GRADE SCREEN ────────────────────────────────────────────────────────── */ function buildGradeScreen() { const totalPts = 15 * 2; const raw = (S.score / totalPts) * 9 + 1; const grade = Math.round(raw * 10) / 10; let cls, msg; if (grade >= 8.5) { cls = ‘excellent’; msg = ‘Uitstekend! 🌟’; } else if (grade >= 7.0) { cls = ‘good’; msg = ‘Goed gedaan! 👍’; } else if (grade >= 5.5) { cls = ‘ok-grade’; msg = ‘Redelijk — blijf oefenen 📚’; } else { cls = ‘poor’; msg = ‘Oefenen helpt! Probeer het opnieuw 💪’; } const wrap = document.createElement(‘div’); wrap.className = ‘card grade-screen fade-up’; wrap.innerHTML = `

    Jouw eindcijfer

    ${grade.toFixed(1)}

    ${msg}

    Punten behaald: ${S.score} van ${totalPts}

    Berekening: (${S.score} ÷ ${totalPts}) × 9 + 1 = ${grade.toFixed(1)}


    `; wrap.appendChild(buildSkillTable()); const btn = document.createElement(‘button’); btn.className = ‘btn btn-primary btn-big’; btn.style.marginTop = ’20px’; btn.textContent = ‘Opnieuw beginnen’; btn.addEventListener(‘click’, doRestart); wrap.appendChild(btn); return wrap; } /* ── MAKER MODE ──────────────────────────────────────────────────────────── */ function buildMaker() { const card = document.createElement(‘div’); card.className = ‘card fade-up’; const savedHtml = S.customExercises.map((ex, i) => `
    ${ex.formulaDisplay}
    ${ex.questionText}
    `).join(”); card.innerHTML = `

    ✏️ Vraagmaker — voeg eigen opgaven toe

    ${S.makerErr ? `` : ”}
    Lineair — eenvoudig Lineair — negatieve getallen Lineair — met haakjes
    ${S.customExercises.length > 0 ? `

    Opgeslagen eigen opgaven (${S.customExercises.length})

    ${savedHtml}` : ”}`; card.querySelector(‘#saveMk’).addEventListener(‘click’, doSaveMaker); card.querySelector(‘#backMk’).addEventListener(‘click’, () => { S.mode = ‘learn’; S.makerErr = null; render(); }); card.querySelectorAll(‘[data-del]’).forEach(btn => { btn.addEventListener(‘click’, () => doDeleteCustom(parseInt(btn.dataset.del))); }); return card; } /* ── TEACHER MODE ────────────────────────────────────────────────────────── */ function buildTeacher() { const json = JSON.stringify(S.customExercises, null, 2); const card = document.createElement(‘div’); card.className = ‘card fade-up’; card.innerHTML = `

    📤 Docentmodus — vraagbank exporteren

    ${S.customExercises.length} eigen opgave(n) opgeslagen.

    `; card.querySelector(‘#dlBtn’).addEventListener(‘click’, doDownload); card.querySelector(‘#backTch’).addEventListener(‘click’, () => { S.mode = ‘learn’; render(); }); return card; } /* ═══════════════════════════════════════════════════════════════════════════ ACTION HANDLERS ═══════════════════════════════════════════════════════════════════════════ */ /* ── CHECK ANSWER ────────────────────────────────────────────────────────── */ function doCheck() { const inp = document.getElementById(‘answerInput’); if (!inp) return; const raw = inp.value.trim(); if (raw === ”) { S.fb = { text: ‘Voer een getal in!’, ok: false }; render(); return; } const val = parseInt(raw, 10); if (isNaN(val)) { S.fb = { text: ‘Voer een geheel getal in (bijv. 7 of -3).’, ok: false }; render(); return; } S.attempts++; if (val === S.ex.answer) { /* ── CORRECT ── */ const pts = S.attempts === 1 ? 2 : S.attempts === 2 ? 1 : 0; S.score += pts; S.streak += 1; S.solved = true; S.exerciseCount++; const ptsLabel = pts === 2 ? ‘ (+2 punten!)’ : pts === 1 ? ‘ (+1 punt)’ : ‘ (+0 punten)’; S.fb = { text: `Juist!${ptsLabel}`, ok: true }; /* Update skill stats */ const tag = S.ex.skillTag; if (!S.skillStats[tag]) S.skillStats[tag] = { correct: 0, total: 0, withHelp: 0 }; S.skillStats[tag].correct++; S.skillStats[tag].total++; if (S.hintLevel > 0) S.skillStats[tag].withHelp++; S.completed.push({ skillTag: tag, attempts: S.attempts, usedHint: S.hintLevel > 0, points: pts }); render(); } else { /* ── WRONG ── */ S.streak = 0; S.fb = { text: diagnoseMistake(val, S.ex, S.attempts), ok: false }; render(); } } /** * Diagnose common mistakes to give targeted feedback. * @param {number} given * @param {Exercise} ex * @param {number} attempts * @returns {string} */ function diagnoseMistake(given, ex, attempts) { const correct = ex.answer; let msg = `Niet juist — jij antwoordde ${given}.`; if (given === -correct && correct !== 0) { msg += ‘ Tip: het teken klopt niet. Let op de mintekens!’; } else if (given === ex.indValue) { msg += ` Tip: je hebt de waarde van ${ex.indVar} ingevuld als antwoord — bereken de formule!`; } else if (Math.abs(given – correct) = 2) { msg += ‘ Gebruik een hint om de stappen te zien.’; } else { msg += ‘ Probeer nog een keer.’; } return msg; } /* ── HINT ────────────────────────────────────────────────────────────────── */ function doHint() { if (S.hintLevel = 15) { S.mode = ‘grade’; render(); return; } /* Reflection after exercise 5 and 10 */ const needsRefl = [5, 10].includes(S.exerciseCount) && !S.reflectionsDone.includes(S.exerciseCount); if (needsRefl) { S.reflectionsDone.push(S.exerciseCount); S.mode = ‘reflection’; S.ex = null; render(); return; } loadNextExercise(); } /* ── CONTINUE AFTER REFLECTION ───────────────────────────────────────────── */ function doContinue() { const ta = document.getElementById(‘reflTa’); if (ta) S.reflectionText = ta.value; S.mode = ‘learn’; loadNextExercise(); } /* ── LOAD NEXT EXERCISE ──────────────────────────────────────────────────── */ function loadNextExercise() { S.ex = generateNext(S); S.hintLevel = 0; S.showSolution = false; S.attempts = 0; S.solved = false; S.fb = null; render(); } /* ── RESTART ─────────────────────────────────────────────────────────────── */ function doRestart() { Object.assign(S, { mode: ‘learn’, ex: null, hintLevel: 0, showSolution: false, attempts: 0, solved: false, fb: null, exerciseCount: 0, score: 0, streak: 0, completed: [], skillStats: { ‘lineair-eenvoudig’: { correct: 0, total: 0, withHelp: 0 }, ‘lineair-negatief’: { correct: 0, total: 0, withHelp: 0 }, ‘lineair-haakjes’: { correct: 0, total: 0, withHelp: 0 } }, workedExampleOpen: false, reflectionText: ”, reflectionsDone: [] }); loadNextExercise(); } /* ── ENTER MAKER MODE ────────────────────────────────────────────────────── */ function enterMaker() { const pw = prompt(‘Voer het wachtwoord in voor de Vraagmaker:’); if (pw === null) return; if (pw !== ‘Bussum2025’) { alert(‘Onjuist wachtwoord.’); return; } S.mode = ‘maker’; S.makerErr = null; render(); } /* ── ENTER TEACHER MODE ──────────────────────────────────────────────────── */ function enterTeacher() { const pw = prompt(‘Voer het wachtwoord in voor de Docentmodus:’); if (pw === null) return; if (pw !== ‘Bussum2025’) { alert(‘Onjuist wachtwoord.’); return; } S.mode = ’teacher’; render(); } /* ── SAVE CUSTOM EXERCISE ────────────────────────────────────────────────── */ function doSaveMaker() { const formula = document.getElementById(‘mk_f’).value.trim(); const question = document.getElementById(‘mk_q’).value.trim(); const ansStr = document.getElementById(‘mk_a’).value.trim(); const h1 = document.getElementById(‘mk_h1’).value.trim(); const h2 = document.getElementById(‘mk_h2’).value.trim(); const h3 = document.getElementById(‘mk_h3’).value.trim(); const solution = document.getElementById(‘mk_sol’).value.trim(); const skill = document.getElementById(‘mk_sk’).value; if (!formula || !question || !h1 || !h2 || !h3 || !solution) { S.makerErr = ‘Vul alle verplichte velden in!’; render(); return; } const answer = parseInt(ansStr, 10); if (isNaN(answer)) { S.makerErr = ‘Het antwoord moet een geheel getal zijn (bijv. 11 of -4).’; render(); return; } S.customExercises.push({ id: uid(), formulaDisplay: formula, questionText: question, depVar: ”, indVar: ”, indValue: 0, answer, hints: [h1, h2, h3], solutionSteps: solution.split(‘\n’).filter(Boolean), skillTag: skill, difficulty: 2, isCustom: true }); saveSaved(S.customExercises); S.makerErr = null; render(); } /* ── DELETE CUSTOM EXERCISE ──────────────────────────────────────────────── */ function doDeleteCustom(idx) { if (!confirm(‘Weet je zeker dat je deze opgave wilt verwijderen?’)) return; S.customExercises.splice(idx, 1); saveSaved(S.customExercises); render(); } /* ── DOWNLOAD JSON ───────────────────────────────────────────────────────── */ function doDownload() { const json = JSON.stringify(S.customExercises, null, 2); const blob = new Blob([json], { type: ‘application/json’ }); const url = URL.createObjectURL(blob); const a = document.createElement(‘a’); a.href = url; a.download = ‘vraagbank-lineaire-formules.json’; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /* ═══════════════════════════════════════════════════════════════════════════ HELPERS ═══════════════════════════════════════════════════════════════════════════ */ function skillLabel(tag) { return { ‘lineair-eenvoudig’: ‘Lineair — eenvoudig’, ‘lineair-negatief’: ‘Negatieve getallen’, ‘lineair-haakjes’: ‘Met haakjes’ }[tag] || tag; } function skillColor(tag) { return { ‘lineair-eenvoudig’: ‘#5b7c5e’, ‘lineair-negatief’: ‘#b5783e’, ‘lineair-haakjes’: ‘#7a6094’ }[tag] || ‘#888’; } /* ═══════════════════════════════════════════════════════════════════════════ INITIALISE ═══════════════════════════════════════════════════════════════════════════ */ runChecks(); S.ex = generateNext(S); render();
  • Balansmethode — Lineaire Vergelijkingen :root { –bg:#F8F5F0;–card:#FFFFFF;–alt:#EDE7DC; –green:#5B8A5E;–gdk:#3A5E3D;–glt:#E8F3E9; –amber:#B87020;–amblt:#FFF8EC;–ambbr:#D9B060; –red:#A84040;–redlt:#FDF2F2; –text:#28241C;–muted:#6A6255;–border:#D6CEBC; –shadow:0 2px 14px rgba(40,36,28,.09);–r:12px; } *{box-sizing:border-box;margin:0;padding:0} html{font-size:16px;scroll-behavior:smooth} body{font-family:-apple-system,BlinkMacSystemFont,’Segoe UI’,sans-serif;background:var(–bg);color:var(–text);min-height:100vh} /* HEADER */ #hdr{background:var(–card);border-bottom:2px solid var(–border);position:sticky;top:0;z-index:200;box-shadow:0 1px 8px rgba(0,0,0,.07)} .hdr-i{max-width:780px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px} .logo{font-weight:800;font-size:1.05rem;color:var(–gdk);display:flex;align-items:center;gap:8px} .logo svg{width:24px;height:24px;flex-shrink:0} nav{display:flex;gap:4px} .nb{padding:5px 13px;border-radius:20px;border:1.5px solid transparent;background:none;cursor:pointer;font-size:.8rem;font-weight:700;color:var(–muted);transition:all .18s} .nb:hover{background:var(–alt);color:var(–text)} .nb.act{background:var(–green);color:#fff;border-color:var(–green)} /* SCORE STRIP */ #strip{background:var(–alt);border-bottom:1px solid var(–border);padding:5px 16px} .strip-i{max-width:780px;margin:0 auto;display:flex;align-items:center;gap:12px} .sbar{flex:1;height:8px;background:var(–border);border-radius:4px;overflow:hidden} .sfil{height:100%;background:var(–green);border-radius:4px;transition:width .5s ease;width:0%} .sstats{display:flex;gap:14px;font-size:.79rem;color:var(–muted);white-space:nowrap} .sstats strong{color:var(–text)} .mode-badge{display:inline-block;padding:2px 9px;border-radius:10px;font-size:.7rem;font-weight:700;background:#FFF0E8;color:#7A3800;border:1.5px solid var(–amber)} /* MAIN */ main{max-width:780px;margin:0 auto;padding:20px 16px 80px} .page{display:none} .page.act{display:block;animation:fu .28s ease} @keyframes fu{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}} /* WELKOMSTPAGINA */ .welcome-wrap{max-width:520px;margin:28px auto} .welcome-hero{text-align:center;margin-bottom:28px} .welcome-hero .big-icon{font-size:3rem;display:block;margin-bottom:10px} .welcome-title{font-size:1.9rem;font-weight:900;color:var(–gdk);margin-bottom:6px} .welcome-sub{font-size:.95rem;color:var(–muted)} .welcome-q{font-size:1rem;font-weight:700;color:var(–text);text-align:center;margin-bottom:16px;padding:13px 16px;background:var(–alt);border-radius:9px;border:1.5px solid var(–border)} .choice-btn{width:100%;display:flex;align-items:center;gap:14px;padding:18px 20px;border-radius:12px;border:2.5px solid transparent;cursor:pointer;text-align:left;margin-bottom:12px;transition:all .2s;background:var(–card);box-shadow:var(–shadow)} .choice-btn:last-child{margin-bottom:0} .choice-icon{font-size:2rem;flex-shrink:0;line-height:1} .choice-title-text{font-size:1rem;font-weight:700;margin-bottom:4px} .choice-sub-text{font-size:.8rem;opacity:.8} .choice-ultra{border-color:#7B3598;color:#4A1060} .choice-ultra:hover{background:#F8F0FC;transform:translateY(-2px);box-shadow:0 6px 20px rgba(123,53,152,.22)} .choice-hard{border-color:var(–amber);color:#6A3A00} .choice-hard:hover{background:#FFF5E6;transform:translateY(-2px);box-shadow:0 6px 20px rgba(184,112,32,.2)} .choice-normal{border-color:var(–green);color:var(–gdk)} .choice-normal:hover{background:#EFF8F0;transform:translateY(-2px);box-shadow:0 6px 20px rgba(91,138,94,.2)} /* KAARTEN */ .card{background:var(–card);border-radius:var(–r);border:1.5px solid var(–border);padding:20px;margin-bottom:16px;box-shadow:var(–shadow)} .card-ttl{font-size:.95rem;font-weight:700;margin-bottom:12px;padding-bottom:8px;border-bottom:1.5px solid var(–border)} /* UITKLAPBAAR */ .coll-btn{width:100%;display:flex;justify-content:space-between;align-items:center;background:var(–alt);border:1.5px solid var(–border);border-radius:var(–r);padding:12px 17px;cursor:pointer;font-size:.92rem;font-weight:700;color:var(–text);margin-bottom:12px;transition:background .2s} .coll-btn:hover{background:#E4DDD3} .coll-btn .arr{transition:transform .3s;font-size:.75rem;color:var(–muted)} .coll-btn.open .arr{transform:rotate(180deg)} .coll-body{display:none;background:var(–alt);border:1.5px solid var(–border);border-radius:var(–r);padding:20px;margin-bottom:14px} .coll-body.open{display:block;animation:fu .25s ease} /* BALANS VISUALISATIE */ .bal-wrap{margin:8px 0} .bal-row{display:flex;align-items:stretch;gap:8px;margin-bottom:2px} .bal-side{flex:1;background:var(–card);border:2px solid var(–border);border-radius:8px;padding:9px 12px;text-align:center;font-family:’Courier New’,monospace;font-size:1.05rem;font-weight:700;min-height:52px;display:flex;align-items:center;justify-content:center;transition:all .3s;flex-wrap:wrap;gap:4px;line-height:1.6} .bal-eq{display:flex;align-items:center;justify-content:center;font-size:1.25rem;font-weight:700;color:var(–muted);flex-shrink:0;width:24px} .bal-op-row{display:flex;align-items:center;gap:8px;padding:3px 0} .bal-op{flex:1;text-align:center;font-size:.82rem;font-weight:800;color:var(–amber);background:var(–amblt);border:1.5px solid var(–ambbr);border-radius:6px;padding:4px 8px} .bal-op-mid{flex-shrink:0;width:24px;text-align:center;font-size:.7rem;font-weight:700;color:var(–amber)} .bal-expl{font-size:.79rem;color:var(–muted);margin:2px 0 5px 2px;font-style:italic} .bal-final .bal-side{background:var(–glt);border-color:var(–green);color:var(–gdk)} .bal-answer-banner{text-align:center;margin-top:12px;padding:10px 16px;background:var(–glt);border-radius:8px;font-weight:700;color:var(–gdk);font-size:.95rem} /* Vereenvoudigingsstap */ .bal-simp-lbl{text-align:center;padding:4px 0 2px;font-size:.82rem;font-weight:700;color:#5B45A0;font-style:italic} .bal-simp-expl{font-size:.79rem;color:#5B45A0;margin:2px 0 5px 2px;font-style:italic} .we-ex-title{font-weight:700;font-size:.9rem;margin:14px 0 8px;color:var(–gdk)} .we-ex-title:first-child{margin-top:0} /* VERGELIJKING DISPLAY */ .eq-display{background:var(–alt);border-radius:10px;border:1.5px solid var(–border);padding:18px 20px;text-align:center;font-family:’Courier New’,monospace;font-size:1.9rem;font-weight:700;letter-spacing:1px;margin-bottom:14px;line-height:2.4} @media(max-width:480px){.eq-display{font-size:1.35rem}} /* VRAAG META */ .q-meta{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap} .q-num{font-size:.8rem;color:var(–muted)} .diff-badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:.7rem;font-weight:700} .d-easy{background:var(–glt);color:var(–gdk)} .d-medium{background:var(–amblt);color:#7A4E08} .d-hard{background:var(–redlt);color:var(–red)} .d-ultra{background:#F3E8FC;color:#4A1060} .s-tags{display:flex;gap:5px;flex-wrap:wrap} .s-tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:.68rem;background:var(–alt);border:1px solid var(–border);color:var(–muted)} .s-tag.hard-tag{background:#FFF0E8;border-color:var(–ambbr);color:#7A4E08} .s-tag.ultra-tag{background:#F3E8FC;border-color:#C090D8;color:#4A1060} #help-badge{font-size:.7rem;background:var(–amblt);color:#7A4E08;border:1px solid var(–ambbr);border-radius:6px;padding:2px 8px} /* POGINGSDOTS */ .dots{display:flex;gap:5px;margin-bottom:10px} .dot{width:10px;height:10px;border-radius:50%;background:var(–border)} .dot.used{background:var(–amber)} .dot.ok1{background:var(–green)} .dot.ok2{background:var(–green);opacity:.6} /* INVOERVELD */ .inp-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px} .inp-lbl{font-size:1.1rem;font-weight:700} .ans-inp{width:110px;padding:8px 12px;border:2px solid var(–border);border-radius:8px;font-size:1.1rem;font-weight:700;text-align:center;font-family:’Courier New’,monospace;outline:none;transition:border-color .2s,background .2s} .ans-inp:focus{border-color:var(–green)} .ans-inp.ok{border-color:var(–green);background:var(–glt)} .ans-inp.bad{border-color:var(–red);background:var(–redlt)} .inp-tip{font-size:.76rem;color:var(–muted);font-style:italic} /* KNOPPEN */ .btn{padding:8px 17px;border-radius:8px;border:none;cursor:pointer;font-size:.87rem;font-weight:700;transition:all .18s} .btn:disabled{opacity:.4;cursor:default} .b-pri{background:var(–green);color:#fff} .b-pri:hover:not(:disabled){background:var(–gdk)} .b-hint{background:var(–amblt);color:#7A4E08;border:1.5px solid var(–ambbr)} .b-hint:hover:not(:disabled){background:#FFECD8} .b-ghost{background:none;color:var(–muted);border:1.5px solid var(–border)} .b-ghost:hover:not(:disabled){background:var(–alt)} .b-nxt{background:var(–green);color:#fff;padding:11px 26px;font-size:.95rem} .b-nxt:hover{background:var(–gdk)} .b-restart{background:var(–amblt);color:var(–amber);border:1.5px solid var(–ambbr);padding:10px 22px} /* FEEDBACK */ .fb{display:none;padding:11px 15px;border-radius:9px;margin-bottom:8px;font-size:.9rem} .fb.ok{display:flex;gap:9px;align-items:flex-start;background:var(–glt);border:1.5px solid var(–green);color:var(–gdk)} .fb.bad{display:flex;gap:9px;align-items:flex-start;background:var(–redlt);border:1.5px solid #D09090;color:var(–red)} .fb-ico{font-size:1.1rem;flex-shrink:0;margin-top:1px} .fb-ttl{font-weight:700;display:block;margin-bottom:2px;font-size:.92rem} .fb-dtl{font-size:.84rem;opacity:.9} /* HINTBOX */ .hint-box{display:none;padding:13px 15px;background:var(–amblt);border:1.5px solid var(–ambbr);border-radius:9px;margin-bottom:8px} .hint-box.vis{display:block;animation:fu .25s ease} .h-item{padding:6px 10px;background:rgba(255,255,255,.65);border-radius:6px;margin-bottom:5px;font-size:.88rem} .h-item:last-child{margin-bottom:0} .h-num{font-weight:700;color:#8B5E00;margin-right:4px} /* OPLOSSINGSGEBIED */ .sol-area{display:none;padding:15px;background:var(–alt);border:1.5px solid var(–border);border-radius:9px;margin-bottom:8px} .sol-area.vis{display:block;animation:fu .25s ease} .sol-ttl{font-size:.88rem;font-weight:700;margin-bottom:11px;color:var(–text)} .act-row{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:8px} .nav-row{display:flex;justify-content:flex-end;margin-top:8px} .regel-banner{display:flex;align-items:center;gap:10px;background:var(–glt);border:1.5px solid var(–green);border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:.88rem;color:var(–gdk)} /* REFLECTIE */ .refl-card{background:linear-gradient(135deg,var(–glt),#F2EDE5);border:2px solid var(–green);border-radius:var(–r);padding:24px;margin-bottom:16px} .refl-card h2{color:var(–gdk);font-size:1.1rem;margin-bottom:4px} .refl-card>p{color:var(–muted);font-size:.87rem;margin-bottom:16px} .refl-q{font-weight:700;font-size:.88rem;margin-bottom:6px;margin-top:12px} textarea.refl-ta{width:100%;border:1.5px solid var(–border);border-radius:8px;padding:9px 11px;font-family:inherit;font-size:.88rem;resize:vertical;min-height:70px;background:#fff;outline:none;transition:border-color .2s} textarea.refl-ta:focus{border-color:var(–green)} /* CIJFERPAGINA */ .grade-wrap{text-align:center;padding:36px 20px} .grade-lbl{font-size:.95rem;color:var(–muted);font-weight:700} .grade-big{font-size:8rem;font-weight:900;line-height:1;margin:10px 0 14px} .gc-A{color:var(–gdk)}.gc-B{color:#6B9E5B}.gc-C{color:var(–amber)}.gc-D{color:var(–red)} .grade-msg{font-size:1.1rem;font-weight:600;margin-bottom:22px} .grade-tbl{max-width:380px;margin:0 auto 22px;border-radius:10px;overflow:hidden;border:1.5px solid var(–border)} .g-row{display:flex;justify-content:space-between;padding:8px 14px;font-size:.88rem} .g-row:nth-child(odd){background:var(–alt)} .g-row:nth-child(even){background:var(–card)} .g-pts{font-weight:700;color:var(–gdk)} /* VOORTGANG */ .stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:18px} @media(max-width:480px){.stat-grid{grid-template-columns:repeat(2,1fr)}} .stat-card{background:var(–card);border:1.5px solid var(–border);border-radius:10px;padding:14px;text-align:center;box-shadow:var(–shadow)} .stat-n{font-size:1.9rem;font-weight:800;color:var(–gdk)} .stat-l{font-size:.76rem;color:var(–muted);margin-top:2px} .skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px;margin-bottom:18px} .skill-card{background:var(–card);border:1.5px solid var(–border);border-radius:10px;padding:14px;box-shadow:var(–shadow)} .sk-name{font-size:.83rem;font-weight:700;margin-bottom:7px} .sk-bar{height:7px;background:var(–border);border-radius:4px;overflow:hidden;margin-bottom:5px} .sk-fil{height:100%;background:var(–green);border-radius:4px} .sk-stat{font-size:.75rem;color:var(–muted)} .hist-list{display:flex;flex-direction:column;gap:5px} .hist-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(–card);border:1.5px solid var(–border);border-radius:7px;font-size:.83rem} .h-eq{font-family:’Courier New’,monospace;font-weight:700;flex:1} .h-pts{font-weight:700;color:var(–gdk)} .h-pts.z{color:var(–muted)} .h-help{display:inline-block;font-size:.7rem;background:var(–amblt);color:#7A4E08;border:1px solid var(–ambbr);border-radius:6px;padding:1px 6px} /* VRAAGMAKER */ .sec-ttl{font-size:.95rem;font-weight:700;margin-bottom:12px;padding-bottom:7px;border-bottom:2px solid var(–border)} .fg{margin-bottom:12px} .fl{display:block;font-size:.8rem;font-weight:700;color:var(–muted);margin-bottom:4px} input.fi,textarea.fi,select.fi{width:100%;padding:8px 11px;border:1.5px solid var(–border);border-radius:8px;font-family:inherit;font-size:.88rem;background:#fff;outline:none;transition:border-color .2s} input.fi:focus,textarea.fi:focus,select.fi:focus{border-color:var(–green)} textarea.fi{resize:vertical;min-height:54px} .cq-list{display:flex;flex-direction:column;gap:7px;margin-top:11px} .cq-item{display:flex;align-items:center;gap:8px;padding:9px 13px;background:var(–card);border:1.5px solid var(–border);border-radius:8px} .cq-eq{font-family:’Courier New’,monospace;font-size:.88rem;font-weight:700;flex:1} .cq-del{background:none;border:none;cursor:pointer;color:var(–muted);font-size:1rem;padding:2px 5px;border-radius:4px} .cq-del:hover{background:var(–redlt);color:var(–red)} /* DOCENT */ .json-out{width:100%;min-height:180px;max-height:360px;font-family:’Courier New’,monospace;font-size:.76rem;border:1.5px solid var(–border);border-radius:8px;padding:11px;background:#1C1C1C;color:#D4D4D4;resize:vertical;outline:none} /* MODAAL */ .modal{position:fixed;inset:0;background:rgba(0,0,0,.32);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999} .modal.hidden{display:none} .modal-card{background:var(–card);border-radius:var(–r);padding:26px;width:310px;box-shadow:0 8px 40px rgba(0,0,0,.2)} .m-ttl{font-size:1rem;font-weight:700;margin-bottom:4px} .m-sub{font-size:.83rem;color:var(–muted);margin-bottom:14px} .m-err{color:var(–red);font-size:.82rem;margin-top:5px;min-height:18px} .m-btns{display:flex;gap:7px;margin-top:13px} .scale-deco{display:block;margin:0 auto 14px} .hidden{display:none!important} /* ── ECHTE BREUKWEERGAVE ── */ .frac{display:inline-flex;flex-direction:column;align-items:center;vertical-align:middle;margin:0 3px;line-height:1.15} .frac .num{border-bottom:2px solid currentColor;padding:1px 5px 2px;text-align:center;min-width:1em;font-size:.88em} .frac .den{padding:2px 5px 1px;text-align:center;min-width:1em;font-size:.88em} .tc{text-align:center} .small{font-size:.82rem;color:var(–muted)} .mt8{margin-top:8px}
    Vraag / Score 0 pt Reeks 0 🔥
    ⚖️

    Balansmethode

    Lineaire vergelijkingen oplossen

    Klas H9 · 1 havo/vwo

    Kies je niveau:
    ⚖️
    De gulden regel: Elke bewerking doe je aan beide kanten — zo blijft de weegschaal in evenwicht!
    Voorbeeld 1 — Los op: 2x + 3 = 7
    Voorbeeld 2 — Los op: 6a + 5 = 2a + 11 (letter maakt niet uit!)
    Voorbeeld 3 — Los op: x/2 + 3 = 7 (💥 breuk: vermenigvuldig met de noemer!)
    Voorbeeld 4 — Los op: 2(x + 1) = 10 (💥 haakjes: werk eerst uit!)

    💡 Oranje balkjes = zelfde bewerking aan beide kanten. Paarse pijl = vereenvoudigen (haakjes/noemer).

    Vraag 1 van 15 Gemiddeld
    x =
    ⚖️ Uitwerking stap voor stap — balansmethode

    🌱 Even nadenken…

    Je hebt 5 opgaven gemaakt.

    Wat was je aanpak bij de moeilijkste vraag?
    Welke stap vond je het lastigst? Waarom?
    Jouw eindcijfer
    📊 Jouw voortgang
    0
    Punten behaald
    0
    Opgaven gedaan
    0
    Beste reeks
    Per vaardigheid
    Geschiedenis
    ➕ Nieuwe vraag toevoegen
    MakkelijkGemiddeldMoeilijk
    📚 Opgeslagen eigen vragen
    👩‍🏫 Docentmodus — Exporteer vraagbank

    Alle ingebouwde én eigen vragen als JSON.

    ‘use strict’; const PW=’Bussum2025′,LS_CUSTOM=’bm_custom_questions’; const NORM_PLAN=[‘hard’,’hard’,’medium’,’hard’,’medium’,’medium’,’medium’,’easy’,’medium’,’easy’,’easy’,’easy’,’easy’,’medium’,’easy’]; const HARD_PLAN=[‘hard’,’neg’,’frac’,’hard’,’medium’,’neg’,’frac’,’hard’,’neg’,’frac’,’hard’,’medium’,’neg’,’frac’,’hard’,’neg’,’medium’,’frac’,’hard’,’neg’]; const ULTRA_PLAN=[‘ufrac1′,’ubracket1′,’ufrac2′,’ubracket2′,’ufrac1′,’ubracket1′,’ufrac2′,’ubracket2′,’ufrac1′,’ubracket1′,’ufrac2′,’ubracket2′,’ufrac1′,’ubracket1′,’ufrac2′,’ubracket2′,’ufrac1′,’ubracket2′,’ufrac2′,’ubracket1’]; function ri(a,b){return Math.floor(Math.random()*(b-a+1))+a} function assert(c,m){if(!c)console.error(‘[BALANS]’,m)} let _u=0;function uid(){return’eq_’+Date.now()+’_’+(++_u)} function fmtN(n){return Number.isInteger(n)?String(n):String(n).replace(‘.’,’,’)} function parseInput(raw){ const s=raw.replace(‘\u2212′,’-‘).replace(‘,’,’.’).trim(); if(s.includes(‘/’)){const[a,b]=s.split(‘/’);const n=parseFloat(a.trim()),d=parseFloat(b.trim());return(isFinite(n)&&isFinite(d)&&d!==0)?n/d:NaN} return parseFloat(s) } function validateEq(eq){ assert(typeof eq.answer===’number’&&isFinite(eq.answer)&&eq.answer!==0,’Antwoord ongeldig: ‘+eq.answer+’ voor ‘+eq.display); assert(eq.hints.length===3,’3 hints vereist’); assert(eq.steps.length>=2,’Min 2 stappen’); assert(eq.steps[0].op===null,’Eerste stap op=null’); const last=eq.steps[eq.steps.length-1]; assert(last.right===fmtN(eq.answer),’Laatste stap rechts “‘+last.right+’” != fmtN(‘+eq.answer+’)=”‘+fmtN(eq.answer)+’”‘) } /* ── NORMAAL NIVEAU ── */ function gen1(){ const x=ri(1,12),a=ri(1,9),sub=Math.random()>.5&&x>a; let display,steps,hints; if(sub){const b=x-a;display=`x \u2212 ${a} = ${b}`; steps=[{left:`x \u2212 ${a}`,right:`${b}`,op:null,expl:’Startvergelijking.’},{left:’x’,right:fmtN(x),op:`+ ${a}`,expl:`Tel ${a} op bij beide kanten`}]; hints=[`Er wordt ${a} afgetrokken. Hoe maak je dat ongedaan?`,`Tel ${a} op bij \u2329beide kanten\u232a.`,`x \u2212 ${a} + ${a} = ${b} + ${a} \u2192 x = ${x}`]; }else{const b=x+a;display=`x + ${a} = ${b}`; steps=[{left:`x + ${a}`,right:`${b}`,op:null,expl:’Startvergelijking.’},{left:’x’,right:fmtN(x),op:`\u2212 ${a}`,expl:`Trek ${a} af van beide kanten`}]; hints=[`Er wordt ${a} opgeteld. Hoe maak je dat ongedaan?`,`Trek ${a} af van \u2329beide kanten\u232a.`,`x = ${x}`]; } const eq={id:uid(),type:’x\u00b1a=b’,difficulty:’easy’,display,answer:x,steps,hints,fullText:”,skillTags:[‘\u00e9\u00e9n stap’,’optellen/aftrekken’],custom:false}; validateEq(eq);return eq } function gen2(){ const a=ri(2,9),x=ri(1,9),b=a*x; const display=`${a}x = ${b}`; const steps=[{left:`${a}x`,right:`${b}`,op:null,expl:’Startvergelijking.’},{left:’x’,right:fmtN(x),op:`\u00f7 ${a}`,expl:`Deel beide kanten door ${a}`}]; const hints=[`Er staat ${a} voor x. Hoe krijg je x alleen?`,`Deel \u2329beide kanten\u232a door ${a}.`,`x = ${x}`]; const eq={id:uid(),type:’ax=b’,difficulty:’easy’,display,answer:x,steps,hints,fullText:”,skillTags:[‘\u00e9\u00e9n stap’,’vermenigvuldigen/delen’],custom:false}; validateEq(eq);return eq } function gen3(){ const a=ri(2,5),x=ri(1,8),b=ri(1,9),useSub=Math.random()>.4; let display,steps,hints; if(useSub){const c=a*x-b;if(c25)return genNeg2(); const display=`${a}x + ${b} = ${c}`; const steps=[{left:`${a}x + ${b}`,right:`${c}`,op:null,expl:’Startvergelijking.’},{left:`${a}x`,right:`${a*x}`,op:`\u2212 ${b}`,expl:`Trek ${b} af`},{left:’x’,right:fmtN(x),op:`\u00f7 ${a}`,expl:`Deel door ${a}`}]; const hints=[`Twee stappen. Begin met ${b} wegwerken.`,`Trek ${b} af \u2192 ${a}x = ${a*x}`,`Deel door ${a} \u2192 x = ${x} (negatief!)`]; const eq={id:uid(),type:’ax+b=c’,difficulty:’hard’,display,answer:x,steps,hints,fullText:”,skillTags:[‘negatief antwoord’,’twee stappen’],custom:false}; validateEq(eq);return eq } function genNeg3(){ const diff=ri(1,3),c=ri(1,3),a=c+diff,x=ri(-4,-1),b=ri(1,8),d=diff*x+b; if(Math.abs(d)>18)return genNeg3(); const dStr=d>=0?`${c}x + ${d}`:`${c}x \u2212 ${Math.abs(d)}`; const rhs2=d>=0?`${d}`:`\u2212${Math.abs(d)}`; const display=`${a}x + ${b} = ${dStr}`; const steps=[{left:`${a}x + ${b}`,right:(d>=0?`${c}x + ${d}`:`${c}x \u2212 ${Math.abs(d)}`),op:null,expl:’Startvergelijking.’},{left:`${diff}x + ${b}`,right:rhs2,op:`\u2212 ${c}x`,expl:`Trek ${c}x af`},{left:`${diff}x`,right:`${diff*x}`,op:`\u2212 ${b}`,expl:`Trek ${b} af`},{left:’x’,right:fmtN(x),op:`\u00f7 ${diff}`,expl:`Deel door ${diff}`}]; const hints=[`x-termen beide kanten.`,`Trek ${c}x af \u2192 ${diff}x + ${b} = ${rhs2}`,`Trek ${b} af \u2192 ${diff}x = ${diff*x}. Deel door ${diff} \u2192 x = ${x}`]; const eq={id:uid(),type:’ax+b=cx+d’,difficulty:’hard’,display,answer:x,steps,hints,fullText:”,skillTags:[‘negatief antwoord’,’drie stappen’],custom:false}; validateEq(eq);return eq } function pickFrac(){const r=Math.random();return r<.33?genFrac1():r<.66?genFrac2():genFrac3()} function pickNeg(){const r=Math.random();return r<.33?genNeg1():r<.66?genNeg2():genNeg3()} /* ── ULTIEM MOEILIJK: BREUKEN IN VERGELIJKING ── */ function genUFrac1(){ const a=ri(2,6),x=a*ri(1,7),b=ri(1,8),c=x/a+b,ab=a*b,ac=a*c; const display=`x/${a} + ${b} = ${c}`; const steps=[{left:`x/${a} + ${b}`,right:`${c}`,op:null,expl:'Startvergelijking met breuk!'},{left:`x + ${ab}`,right:`${ac}`,op:`\u00d7 ${a}`,expl:`Vermenigvuldig beide kanten met ${a} (noemer wegwerken): x/${a}\u00d7${a}=x, ${b}\u00d7${a}=${ab}, ${c}\u00d7${a}=${ac}`},{left:'x',right:fmtN(x),op:`\u2212 ${ab}`,expl:`Trek ${ab} af van beide kanten`}]; const hints=[`Er is een breuk met noemer ${a}. Hoe maak je die weg?`,`Vermenigvuldig \u2329beide kanten\u232a met ${a} (noemer verdwijnt).`,`Na \u00d7${a}: x + ${ab} = ${ac}. Trek ${ab} af \u2192 x = ${x}`]; const eq={id:uid(),type:'x/a+b=c',difficulty:'hard',display,answer:x,steps,hints,fullText:'',skillTags:['breuk in vergelijking','noemer wegwerken'],custom:false}; validateEq(eq);return eq } function genUFrac2(){ const a=ri(2,6),c=ri(2,8),b=ri(1,8),x=a*c-b,ac=a*c; if(x40)return genUFrac2(); const display=`(x + ${b})/${a} = ${c}`; const steps=[{left:`(x + ${b})/${a}`,right:`${c}`,op:null,expl:’De hele som staat boven de breukstreep.’},{left:`x + ${b}`,right:`${ac}`,op:`\u00d7 ${a}`,expl:`Vermenigvuldig beide kanten met ${a}: (x+${b})/${a}\u00d7${a}=x+${b}, ${c}\u00d7${a}=${ac}`},{left:’x’,right:fmtN(x),op:`\u2212 ${b}`,expl:`Trek ${b} af van beide kanten`}]; const hints=[`Breuk met noemer ${a}. Vermenigvuldig beide kanten met ${a} om de breukstreep te laten verdwijnen.`,`Na \u00d7${a}: x + ${b} = ${ac}.`,`Trek ${b} af \u2192 x = ${x}`]; const eq={id:uid(),type:'(x+b)/a=c’,difficulty:’hard’,display,answer:x,steps,hints,fullText:”,skillTags:[‘breuk in vergelijking’,’noemer wegwerken’],custom:false}; validateEq(eq);return eq } /* ── ULTIEM MOEILIJK: HAAKJES ── */ function genUBracket1(){ const a=ri(2,5),b=ri(1,6),x=ri(1,8),c=a*(x+b),ab=a*b,ax=a*x; const display=`${a}(x + ${b}) = ${c}`; const steps=[{left:`${a}(x + ${b})`,right:`${c}`,op:null,expl:’Startvergelijking met haakjes!’},{left:`${a}x + ${ab}`,right:`${c}`,op:’haakjes uitwerken’,simp:true,expl:`${a}\u00d7x=${a}x, ${a}\u00d7${b}=${ab}`},{left:`${a}x`,right:`${ax}`,op:`\u2212 ${ab}`,expl:`Trek ${ab} af van beide kanten`},{left:’x’,right:fmtN(x),op:`\u00f7 ${a}`,expl:`Deel beide kanten door ${a}`}]; const hints=[`Er zijn haakjes. Werk eerst de haakjes uit door ${a} met elk getal ertussen te vermenigvuldigen.`,`Haakjes: ${a}(x+${b})=${a}x+${ab}. Dan: ${a}x+${ab}=${c}`,`Trek ${ab} af \u2192 ${a}x=${ax}. Deel door ${a} \u2192 x=${x}`]; const eq={id:uid(),type:’a(x+b)=c’,difficulty:’hard’,display,answer:x,steps,hints,fullText:”,skillTags:[‘haakjes uitwerken’,’drie stappen’],custom:false}; validateEq(eq);return eq } function genUBracket2(){ const a=ri(2,4),b=ri(1,5),cv=ri(1,3),x=ri(1,7); const d=a*(x+b)+cv*x,ab=a*b,combined=a+cv,dx=combined*x; const display=`${a}(x + ${b}) + ${cv}x = ${d}`; const steps=[{left:`${a}(x + ${b}) + ${cv}x`,right:`${d}`,op:null,expl:’Startvergelijking — haakjes en een losse x!’},{left:`${a}x + ${ab} + ${cv}x`,right:`${d}`,op:’haakjes uitwerken’,simp:true,expl:`${a}\u00d7x=${a}x, ${a}\u00d7${b}=${ab}. De ${cv}x blijft staan.`},{left:`${combined}x + ${ab}`,right:`${d}`,op:’x-termen samenvatten’,simp:true,expl:`${a}x + ${cv}x = ${combined}x`},{left:`${combined}x`,right:`${dx}`,op:`\u2212 ${ab}`,expl:`Trek ${ab} af van beide kanten`},{left:’x’,right:fmtN(x),op:`\u00f7 ${combined}`,expl:`Deel beide kanten door ${combined}`}]; const hints=[`Haakjes EN losse x. Begin met haakjes uitwerken.`,`${a}(x+${b})=${a}x+${ab}. Dan twee x-termen: ${a}x+${cv}x.`,`Voeg samen: ${combined}x+${ab}=${d}. Trek ${ab} af \u2192 ${combined}x=${dx}. Deel door ${combined} \u2192 x=${x}`]; const eq={id:uid(),type:’a(x+b)+cx=d’,difficulty:’hard’,display,answer:x,steps,hints,fullText:”,skillTags:[‘haakjes uitwerken’,’x-termen samenvatten’],custom:false}; validateEq(eq);return eq } function genByType(t){ if(t===’hard’)return gen4();if(t===’medium’)return gen3();if(t===’easy’)return Math.random()>.5?gen1():gen2(); if(t===’frac’)return pickFrac();if(t===’neg’)return pickNeg(); if(t===’ufrac1′)return genUFrac1();if(t===’ufrac2′)return genUFrac2(); if(t===’ubracket1′)return genUBracket1();if(t===’ubracket2′)return genUBracket2(); return gen3() } function buildQueue(customQ,mode){ const plan=mode===’ultra’?ULTRA_PLAN:mode===’hard’?HARD_PLAN:NORM_PLAN; const queue=[],custom=customQ.filter(q=>q.custom); const toInject=Math.min(3,custom.length),injectAt=new Set(); while(injectAt.size<toInject)injectAt.add(ri(0,plan.length-1)); const pool=[…custom]; for(let i=0;i0){const idx=ri(0,pool.length-1);queue.push({…pool[idx]});pool.splice(idx,1)} else queue.push(genByType(plan[i])) } queue.forEach((eq,i)=>{assert(typeof eq.answer===’number’&&isFinite(eq.answer),’Q’+(i+1)+’ ongeldig antwoord’);assert(eq.hints.length===3,’Q’+(i+1)+’ 3 hints’)}); return queue } /* STAAT */ const S={queue:[],qIdx:0,attempts:0,hintsShown:0,solShown:false,usedHint:false,answered:false,locked:false,history:[],totalScore:0,streak:0,bestStreak:0,customQ:[],reflections:[],page:’welcome’,pendingPts:2,mode:”,totalQ:15,reflAt:[5,10],started:false}; function loadCustomQ(){try{const r=localStorage.getItem(LS_CUSTOM);if(r)S.customQ=JSON.parse(r)}catch(_){S.customQ=[]}} function saveCustomQ(){try{localStorage.setItem(LS_CUSTOM,JSON.stringify(S.customQ))}catch(_){}} function setMode(mode){ S.mode=mode;S.totalQ=(mode===’hard’||mode===’ultra’)?20:15; S.reflAt=(mode===’hard’||mode===’ultra’)?[7,14]:[5,10]; S.started=true;S.queue=buildQueue(S.customQ,mode); S.qIdx=0;S.history=[];S.totalScore=0;S.streak=0;S.bestStreak=0;S.reflections=[]; document.getElementById(‘mode-badge’).classList.toggle(‘hidden’,mode!==’hard’); document.getElementById(‘ultra-badge’).classList.toggle(‘hidden’,mode!==’ultra’); nav(‘practice’);loadQ(0) } function getDiagnosis(eq,userAns){ const c=eq.answer,d=Math.abs(userAns-c); if(userAns===-c&&c!==0)return’Let op het teken! Is het antwoord misschien negatief?’; if(d0&&c `${num}${den}`); // woord_of_variabele/getal → echte breuk (bijv. x/2, 3x/4) str=str.replace(/([a-zA-Z0-9]+)\/(\d+)/g,(_,num,den)=> `${num}${den}`); return str } function renderBal(steps){ let html=’
    ‘; for(let i=0;i<steps.length;i++){ const s=steps[i],isFinal=i===steps.length-1,rowCls=isFinal?'bal-row bal-final':'bal-row'; const L=fracToHTML(s.left),R=fracToHTML(s.right),EX=fracToHTML(s.expl); if(i===0){ html+=`
    ${L}
    =
    ${R}
    `; }else if(s.simp){ html+=`
    \u2193 ${s.op}
    ${EX}
    ${L}
    =
    ${R}
    `; }else{ html+=`
    ${s.op}
    \u21c5
    ${s.op}
    ${EX}
    ${L}
    =
    ${R}
    `; } } const last=steps[steps.length-1]; html+=`
    \u2713 Antwoord: x = ${fracToHTML(last.right)}
    `; return html } function diffLbl(d){return d===’easy’?’Makkelijk’:d===’medium’?’Gemiddeld’:’Moeilijk’} function diffCls(d){return d===’easy’?’d-easy’:d===’medium’?’d-medium’:’d-hard’} function updateStrip(){ document.getElementById(‘sfil’).style.width=(S.qIdx/S.totalQ*100)+’%’; document.getElementById(‘s-q’).textContent=Math.min(S.qIdx+1,S.totalQ); document.getElementById(‘s-tot’).textContent=S.totalQ; document.getElementById(‘s-sc’).textContent=S.totalScore; document.getElementById(‘s-str’).textContent=S.streak } function loadQ(idx){ if(idx>=S.totalQ){showGrade();return} const eq=S.queue[idx]; S.attempts=0;S.hintsShown=0;S.solShown=false;S.usedHint=false;S.answered=false;S.locked=false;S.pendingPts=2; document.getElementById(‘q-num’).textContent=`Vraag ${idx+1} van ${S.totalQ}`; const badge=document.getElementById(‘diff-badge’); if(S.mode===’ultra’){badge.className=’diff-badge d-ultra’;badge.textContent=’Ultiem moeilijk’} else{badge.textContent=diffLbl(eq.difficulty);badge.className=’diff-badge ‘+diffCls(eq.difficulty)} document.getElementById(‘skill-tags’).innerHTML=eq.skillTags.map(t=>{ const isU=[‘breuk in vergelijking’,’noemer wegwerken’,’haakjes uitwerken’,’x-termen samenvatten’].includes(t); const isH=[‘kommagetal’,’negatief antwoord’].includes(t); return`${t}` }).join(”); document.getElementById(‘help-badge’).classList.add(‘hidden’); document.getElementById(‘eq-disp’).innerHTML=fracToHTML(eq.display); const tip=document.getElementById(‘inp-tip’); if(eq.skillTags.includes(‘kommagetal’)){tip.textContent=’💡 Kommagetal mogelijk (bijv. 1,5)’;tip.classList.remove(‘hidden’)} else if(eq.skillTags.includes(‘negatief antwoord’)){tip.textContent=’💡 Negatief antwoord mogelijk’;tip.classList.remove(‘hidden’)} else if([‘breuk in vergelijking’,’haakjes uitwerken’].some(t=>eq.skillTags.includes(t))){tip.textContent=’💥 Vermenigvuldig met de noemer — of werk de haakjes uit’;tip.classList.remove(‘hidden’)} else tip.classList.add(‘hidden’); for(let i=0;i<3;i++)document.getElementById('d'+i).className='dot'; const inp=document.getElementById('ans');inp.value='';inp.className='ans-inp';inp.disabled=false;inp.focus(); document.getElementById('btn-chk').disabled=false; document.getElementById('btn-hint').disabled=false; document.getElementById('btn-hint').textContent='Hint'; document.getElementById('btn-sol').disabled=false; document.getElementById('btn-nxt').classList.add('hidden'); document.getElementById('fb').className='fb'; document.getElementById('hint-box').className='hint-box';document.getElementById('hint-box').innerHTML=''; document.getElementById('sol-area').className='sol-area';document.getElementById('sol-content').innerHTML=''; updateStrip(); document.getElementById('q-card').scrollIntoView({behavior:'smooth',block:'start'}) } function checkAns(){ if(S.locked)return; const eq=S.queue[S.qIdx],inp=document.getElementById('ans'),raw=inp.value.trim(); if(!raw){inp.focus();return} const userAns=parseInput(raw); if(isNaN(userAns)){showFB(false,'Geen geldig getal','Voer een getal in, bijv. 4, \u22122 of 1,5.');return} S.attempts++; const dotIdx=Math.min(S.attempts-1,2); if(S.solShown)S.pendingPts=0; else if(S.attempts===1)S.pendingPts=2; else if(S.attempts===2)S.pendingPts=1; else S.pendingPts=0; const correct=Math.abs(userAns-eq.answer)S.bestStreak)S.bestStreak=S.streak; inp.disabled=true; document.getElementById(‘btn-chk’).disabled=true;document.getElementById(‘btn-hint’).disabled=true;document.getElementById(‘btn-sol’).disabled=true; const ptsTxt=S.pendingPts===2?'(+2 punten — direct goed! \u2b50)’:S.pendingPts===1?'(+1 punt — goed gedaan!)’:'(+0 punten)’; showFB(true,’Goed gedaan! \u2713′,ptsTxt); recordAttempt(eq,true,S.pendingPts); document.getElementById(‘btn-nxt’).classList.remove(‘hidden’);updateStrip() }else{ document.getElementById(‘d’+dotIdx).classList.add(‘used’);S.streak=0; const diag=getDiagnosis(eq,userAns); if(S.attempts>=3){ S.locked=true;S.pendingPts=0;inp.disabled=true;document.getElementById(‘btn-chk’).disabled=true; recordAttempt(eq,false,0);showFB(false,’Niet goed.’,`${diag} Bekijk de uitwerking.`); document.getElementById(‘btn-nxt’).classList.remove(‘hidden’) }else{ const left=3-S.attempts;showFB(false,’Niet helemaal goed.’,`${diag} Nog ${left} poging${left===1?”:’en’}.`); inp.className=’ans-inp bad’;setTimeout(()=>{inp.className=’ans-inp’;inp.select()},700) } } } function recordAttempt(eq,correct,points){ if(S.history.length>S.qIdx)return; S.history.push({eqId:eq.id,display:eq.display,attempts:S.attempts,correct,usedHint:S.usedHint,solShown:S.solShown,points,skillTags:eq.skillTags,difficulty:eq.difficulty}) } function showFB(ok,title,detail){ const fb=document.getElementById(‘fb’);fb.className=’fb ‘+(ok?’ok’:’bad’); document.getElementById(‘fb-ico’).textContent=ok?’✓’:’✗’; document.getElementById(‘fb-ttl’).textContent=title;document.getElementById(‘fb-dtl’).textContent=detail } function nextHint(){ if(S.hintsShown>=3||S.locked)return; S.hintsShown++;S.usedHint=true;document.getElementById(‘help-badge’).classList.remove(‘hidden’); const eq=S.queue[S.qIdx],box=document.getElementById(‘hint-box’); box.className=’hint-box vis’;let html=”; for(let i=0;i<S.hintsShown;i++)html+=`
    Hint ${i+1}:${eq.hints[i]}
    `; box.innerHTML=html; const btn=document.getElementById(‘btn-hint’); if(S.hintsShown>=3){btn.textContent=’Alle hints getoond’;btn.disabled=true}else btn.textContent=`Hint (${S.hintsShown}/3)` } function showSol(){ if(S.solShown)return; S.solShown=true;S.usedHint=true;S.pendingPts=0;S.locked=true; document.getElementById(‘help-badge’).classList.remove(‘hidden’); document.getElementById(‘ans’).disabled=true;document.getElementById(‘btn-chk’).disabled=true; document.getElementById(‘btn-hint’).disabled=true;document.getElementById(‘btn-sol’).disabled=true; const eq=S.queue[S.qIdx];S.hintsShown=3; const hbox=document.getElementById(‘hint-box’);hbox.className=’hint-box vis’; hbox.innerHTML=eq.hints.map((h,i)=>`
    Hint ${i+1}:${h}
    `).join(”); document.getElementById(‘sol-area’).className=’sol-area vis’; document.getElementById(‘sol-content’).innerHTML=eq.custom?`

    ${eq.fullText}

    `:renderBal(eq.steps); document.getElementById(‘btn-nxt’).classList.remove(‘hidden’); recordAttempt(eq,false,0);updateStrip() } function nextQ(){ if(S.history.length=S.totalQ){showGrade();return} if(S.reflAt.includes(S.qIdx)){showRefl(S.qIdx);return} nav(‘practice’);loadQ(S.qIdx) } function showRefl(idx){ document.getElementById(‘refl-sub’).textContent=`Je hebt ${idx} opgaven gemaakt. Neem even de tijd.`; document.getElementById(‘refl-a1’).value=”;document.getElementById(‘refl-a2’).value=”;nav(‘reflection’) } function saveRefl(){ S.reflections.push({at:S.qIdx,q1:document.getElementById(‘refl-a1’).value.trim(),q2:document.getElementById(‘refl-a2’).value.trim()}); nav(‘practice’);loadQ(S.qIdx) } function showGrade(){ const total=S.totalQ*2,earned=S.totalScore; const grade=Math.max(1,Math.min(10,Math.round((earned/total*9+1)*10)/10)); const numEl=document.getElementById(‘grade-num’);numEl.textContent=grade.toFixed(1); numEl.className=grade>=8.5?’grade-big gc-A’:grade>=7?’grade-big gc-B’:grade>=5.5?’grade-big gc-C’:’grade-big gc-D’; const msgs={A:’Uitstekend! Je beheerst de balansmethode perfect! \u2b50′,B:’Heel goed gedaan! \ud83c\udf89′,C:’Goed werk! \ud83d\udc4d’,D:’Voldoende! Oefen nog met de hints. \ud83d\udcaa’,E:’Meer oefenen? Gebruik de balansvisualisatie.’}; const k=grade>=8.5?’A’:grade>=7?’B’:grade>=6?’C’:grade>=5.5?’D’:’E’; document.getElementById(‘grade-msg’).textContent=msgs[k]; const c1=S.history.filter(h=>h.attempts===1&&h.correct).length; const ca=S.history.filter(h=>h.correct).length,wh=S.history.filter(h=>h.usedHint).length; document.getElementById(‘grade-tbl’).innerHTML=`
    Vragen gemaakt${S.history.length}/${S.totalQ}
    Juist beantwoord${ca}
    Direct goed (1e poging)${c1}
    Met hulp${wh}
    Punten${earned}/${total}
    Formule${earned}/${total}\u00d79+1=${grade.toFixed(1)}
    `; document.getElementById(‘sfil’).style.width=’100%’;document.getElementById(‘s-q’).textContent=String(S.totalQ); nav(‘grade’) } function restartPractice(){ S.mode=”;S.started=false;S.totalQ=15;S.reflAt=[5,10];S.queue=[];S.qIdx=0;S.history=[]; S.totalScore=0;S.streak=0;S.bestStreak=0;S.reflections=[]; document.getElementById(‘mode-badge’).classList.add(‘hidden’);document.getElementById(‘ultra-badge’).classList.add(‘hidden’); document.getElementById(‘sfil’).style.width=’0%’;document.getElementById(‘s-q’).textContent=’—’; document.getElementById(‘s-tot’).textContent=’—’;document.getElementById(‘s-sc’).textContent=’0′;document.getElementById(‘s-str’).textContent=’0′; nav(‘welcome’) } function renderProgress(){ document.getElementById(‘p-sc’).textContent=S.totalScore;document.getElementById(‘p-q’).textContent=S.history.length;document.getElementById(‘p-str’).textContent=S.bestStreak; const skills={}; for(const rec of S.history)for(const tag of rec.skillTags){ if(!skills[tag])skills[tag]={total:0,correct:0,help:0}; skills[tag].total++;if(rec.correct)skills[tag].correct++;if(rec.usedHint)skills[tag].help++ } const sg=document.getElementById(‘skill-grid’); if(!Object.keys(skills).length){sg.innerHTML=’

    Nog geen opgaven gedaan.

    ‘} else sg.innerHTML=Object.entries(skills).map(([tag,d])=>{ const pct=d.total>0?Math.round(d.correct/d.total*100):0; return`
    ${tag}
    ${d.correct}/${d.total} goed · ${d.help} met hulp
    ` }).join(”); const hl=document.getElementById(‘hist-list’); if(!S.history.length)hl.innerHTML=’

    Nog geen opgaven gedaan.

    ‘; else hl.innerHTML=S.history.map(rec=>{ const ic=rec.correct?`\u2713`:`\u2717`; const hp=rec.usedHint?’met hulp‘:”; return`
    ${ic}${fracToHTML(rec.display)}${rec.points} pt${hp}
    ` }).join(”) } function nav(id){ document.querySelectorAll(‘.page’).forEach(p=>p.classList.remove(‘act’)); document.querySelectorAll(‘.nb’).forEach(b=>b.classList.remove(‘act’)); const page=document.getElementById(‘page-‘+id);if(page)page.classList.add(‘act’); const map={welcome:’oefenen’,practice:’oefenen’,reflection:’oefenen’,grade:’oefenen’,progress:’progress’,maker:’maker’,teacher:’teacher’}; const navEl=document.getElementById(‘nav-‘+(map[id]||id));if(navEl)navEl.classList.add(‘act’); S.page=id; if(id===’progress’)renderProgress();if(id===’maker’)renderCustomList();if(id===’teacher’)refreshJSON(); window.scrollTo({top:0,behavior:’smooth’}) } function navOefenen(){if(!S.started)nav(‘welcome’);else if(S.page===’grade’)nav(‘grade’);else nav(‘practice’)} function toggleWE(){ const btn=document.getElementById(‘we-btn’),body=document.getElementById(‘we-body’),open=body.classList.contains(‘open’); body.classList.toggle(‘open’,!open);btn.classList.toggle(‘open’,!open);if(!open)renderWE() } function renderWE(){ document.getElementById(‘we-vis-1′).innerHTML=renderBal([ {left:’2x + 3′,right:’7′,op:null,expl:’Startvergelijking.’}, {left:’2x’,right:’4′,op:’\u2212 3′,expl:’Trek 3 af van beide kanten’}, {left:’x’,right:’2′,op:’\u00f7 2′,expl:’Deel beide kanten door 2′} ]); document.getElementById(‘we-vis-2′).innerHTML=renderBal([ {left:’6a + 5′,right:’2a + 11′,op:null,expl:’Startvergelijking (letter a — werkt hetzelfde!).’}, {left:’4a + 5′,right:’11’,op:’\u2212 2a’,expl:’Trek 2a af van beide kanten’}, {left:’4a’,right:’6′,op:’\u2212 5′,expl:’Trek 5 af van beide kanten’}, {left:’a’,right:’1,5′,op:’\u00f7 4′,expl:’Deel beide kanten door 4 (kommagetal!)’} ]); document.getElementById(‘we-vis-3′).innerHTML=renderBal([ {left:’x/2 + 3′,right:’7′,op:null,expl:’Startvergelijking met breuk!’}, {left:’x + 6′,right:’14’,op:’\u00d7 2′,expl:’Vermenigvuldig beide kanten met 2 (noemer wegwerken): x/2\u00d72=x, 3\u00d72=6, 7\u00d72=14′}, {left:’x’,right:’8′,op:’\u2212 6′,expl:’Trek 6 af van beide kanten’} ]); document.getElementById(‘we-vis-4′).innerHTML=renderBal([ {left:’2(x + 1)’,right:’10’,op:null,expl:’Startvergelijking met haakjes!’}, {left:’2x + 2′,right:’10’,op:’haakjes uitwerken’,simp:true,expl:’2\u00d7x+2\u00d71=2x+2′}, {left:’2x’,right:’8′,op:’\u2212 2′,expl:’Trek 2 af van beide kanten’}, {left:’x’,right:’4′,op:’\u00f7 2′,expl:’Deel beide kanten door 2′} ]) } let _pwTarget=”; function reqPw(target){ _pwTarget=target; const labels={maker:’Vraagmaker’,teacher:’Docentmodus’}; document.getElementById(‘m-ttl’).textContent=`Wachtwoord \u2014 ${labels[target]}`; document.getElementById(‘m-sub’).textContent=`Voer het wachtwoord in voor de ${labels[target]}.`; document.getElementById(‘pw-inp’).value=”;document.getElementById(‘m-err’).textContent=”; document.getElementById(‘modal-pw’).classList.remove(‘hidden’); setTimeout(()=>document.getElementById(‘pw-inp’).focus(),80) } function submitPw(){ if(document.getElementById(‘pw-inp’).value===PW){document.getElementById(‘modal-pw’).classList.add(‘hidden’);nav(_pwTarget)} else{document.getElementById(‘m-err’).textContent=’Onjuist wachtwoord.’;document.getElementById(‘pw-inp’).value=”;document.getElementById(‘pw-inp’).focus()} } function closePw(){document.getElementById(‘modal-pw’).classList.add(‘hidden’)} function saveMakerQ(){ const eqTxt=document.getElementById(‘mk-eq’).value.trim(); const ans=parseInput(document.getElementById(‘mk-ans’).value.trim()); const h1=document.getElementById(‘mk-h1’).value.trim(),h2=document.getElementById(‘mk-h2’).value.trim(),h3=document.getElementById(‘mk-h3’).value.trim(); const sol=document.getElementById(‘mk-sol’).value.trim(),diff=document.getElementById(‘mk-diff’).value; const e=document.getElementById(‘mk-err’); if(!eqTxt){e.textContent=’Voer een vergelijking in.’;return} if(isNaN(ans)){e.textContent=’Voer een geldig antwoord in.’;return} if(!h1||!h2||!h3){e.textContent=’Voeg alle 3 hints toe.’;return} if(!sol){e.textContent=’Voeg een uitwerking toe.’;return} e.textContent=”; const parts=eqTxt.split(‘=’); S.customQ.push({id:uid(),type:’custom’,difficulty:diff,display:eqTxt,answer:ans, steps:[{left:(parts[0]||eqTxt).trim(),right:(parts[1]||’?’).trim(),op:null,expl:’Startvergelijking.’},{left:’x’,right:fmtN(ans),op:’…’,expl:’Zie volledige uitwerking.’}], hints:[h1,h2,h3],fullText:sol,skillTags:[‘eigen opgave’],custom:true}); saveCustomQ(); [‘mk-eq’,’mk-h1′,’mk-h2′,’mk-h3′,’mk-sol’].forEach(id=>{document.getElementById(id).value=”}); document.getElementById(‘mk-ans’).value=”;renderCustomList();alert(‘Vraag opgeslagen! \ud83c\udf89’) } function renderCustomList(){ const list=document.getElementById(‘cq-list’); if(!S.customQ.length){list.innerHTML=’

    Nog geen eigen vragen.

    ‘;return} list.innerHTML=S.customQ.map((q,i)=>`
    ${q.display}x=${fmtN(q.answer)}
    `).join(”) } function deleteQ(i){if(confirm(‘Vraag verwijderen?’)){S.customQ.splice(i,1);saveCustomQ();renderCustomList()}} function refreshJSON(){document.getElementById(‘json-out’).value=JSON.stringify([…S.queue,…S.customQ.filter(q=>!S.queue.includes(q))],null,2)} function copyJSON(){const ta=document.getElementById(‘json-out’);(navigator.clipboard?navigator.clipboard.writeText(ta.value):Promise.reject()).then(()=>alert(‘Gekopieerd!’)).catch(()=>{ta.select();document.execCommand(‘copy’);alert(‘Gekopieerd!’)})} function downloadJSON(){const blob=new Blob([document.getElementById(‘json-out’).value],{type:’application/json’});const url=URL.createObjectURL(blob);Object.assign(document.createElement(‘a’),{href:url,download:’vraagbank_balansmethode.json’}).click();URL.revokeObjectURL(url)} function init(){loadCustomQ();renderWE();nav(‘welcome’)} init();
  • Collect

    Like flowers that bloom in unexpected places, every story unfolds with beauty and resilience

    Image for service

    Assemble

    Like flowers that bloom in unexpected places, every story unfolds with beauty and resilience

    Image for service

    Deliver

    Like flowers that bloom in unexpected places, every story unfolds with beauty and resilience