feat(rc3): Crumbblocks UI Mission & Smart Routing 🎨

This commit is contained in:
Branko May Trinkwald
2025-12-23 20:52:00 +01:00
parent e52684d6f8
commit 01a01f53b4
27 changed files with 7551 additions and 25 deletions

628
crumbblocks/bezier.html Normal file
View File

@@ -0,0 +1,628 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter • Bezier Pen + Editor</title>
<style>
:root{ --bg:#0b0b10; --panel:#12121a; --muted:#9aa3b2; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c; }
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none;font-size:.9rem;color:var(--muted)}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:380px 1fr;gap:16px;padding:16px}
@media (max-width:1100px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.col{display:grid;gap:10px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0b1114;font-weight:600}
.btn.warn{background:var(--danger);color:#0b0b10}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:240px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a;touch-action:none}
.badge{font-size:.75rem;color:#0b1114;background:var(--accent);border-radius:999px;padding:2px 8px}
.muted{color:var(--muted)}
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
.anchor{fill:#31e07b;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handle{fill:#ffe08a;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handleLine{stroke:#ffe08a;stroke-width:2.5;stroke-dasharray:4 4; pointer-events:none}
.ghost{fill:none;stroke:#ffffff55;stroke-width:2;stroke-dasharray:6 8}
#bezierUI{ pointer-events:none }
.selected{filter:drop-shadow(0 0 4px #ffd166)}
.hint{font-size:.85rem;color:#cfd5e3;line-height:1.3}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter • <span class="badge">Bezier-Pen</span> + Editor • Replay • SVG/PNG/JSON</h1>
</header>
<div class="wrap">
<aside class="panel">
<h2 style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits „1“</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/Stop</span></label>
<label class="chip"><input type="checkbox" id="frameOn" checked> <span>Crumblines-Frame</span></label>
</div>
<div class="row">
<label>Strichstärke <input type="range" id="stroke" min="4" max="28" step="1" value="12"></label>
<label>Sampler
<select id="sampler">
<option value="raw">Raw Polyline</option>
<option value="quad">Quadratic</option>
<option value="cubic" selected>CatmullRom → Cubic</option>
</select>
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>Stroke A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="col" style="margin-top:6px">
<strong>BezierPen (Anker setzen)</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="penMode"> <span>Pen aktiv</span></label>
<button class="btn" id="finishPen" disabled>Fertig (↵)</button>
<span class="muted">Shift: Winkel snap • Alt: Ecke • ⌫: Letzten Anchor löschen</span>
</div>
</div>
<div class="col" style="margin-top:6px">
<strong>BezierEditor</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="editBezier"> <span>Bearbeiten</span></label>
<label class="chip"><input type="checkbox" id="linkHandles" checked> <span>SpiegelGriffe</span></label>
<button class="btn" id="toBezier">Auswahl → Bezier</button>
<button class="btn" id="splitHandles">Anker: Split</button>
<button class="btn warn" id="delAnchor" disabled>Anker löschen</button>
</div>
<p class="hint">Klicke einen Stroke → <em>Bearbeiten</em>. Doppelklick auf Pfad fügt einen Anker. <span class="kbd">L</span> toggelt SpiegelGriffe.</p>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="Strich-Liste"></ul>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="replay">▶︎ Replay</button>
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn" id="downloadPNG">PNG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
</defs>
<rect id="frameRect" x="16.5" y="16.5" width="1167" height="767" fill="none" stroke="rgba(255,255,255,.2)" stroke-width="1.5" stroke-dasharray="6 10"/>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<path id="ghost" class="ghost" d=""/>
<g id="bezierUI"></g>
</g>
</svg>
</main>
</div>
<footer>
<span>Neu: richtiger BezierPen wie in Illustrator/Inkscape (Anker setzen & ziehen).</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const ghost = document.getElementById('ghost');
const bezierUI= document.getElementById('bezierUI');
const frameRect = document.getElementById('frameRect');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const frameOnCb = document.getElementById('frameOn');
const strokeInp = document.getElementById('stroke');
const samplerSel= document.getElementById('sampler');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const penModeCb = document.getElementById('penMode');
const finishPenBtn = document.getElementById('finishPen');
const editBezierCb = document.getElementById('editBezier');
const linkHandlesCb= document.getElementById('linkHandles');
const toBezierBtn = document.getElementById('toBezier');
const splitBtn = document.getElementById('splitHandles');
const delAnchorBtn = document.getElementById('delAnchor');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const dlPNGBtn = document.getElementById('downloadPNG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
const replayBtn = document.getElementById('replay');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null;
const strokes = [];
let t0 = null;
let selectedStroke = null;
let selectedAnchor = {stroke:null, idx:-1};
let dragging = null; // edit-drag
// Pen state
let penActive = false;
let penStroke = null; // {isBezier:true, anchors:[], pathEl,...}
let penDragging = false;
let placingFirst = false;
let placingNext = false;
let penStartPt = null;
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const p = pt.matrixTransform(stage.getScreenCTM().inverse());
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
p.addEventListener('pointerdown', (e)=>{
if(editBezierCb.checked && !penActive){ e.stopPropagation(); const s = strokes.find(st=>st.pathEl===p); if(s){ selectStroke(s); drawBezierUI(); } }
});
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function pathFromAnchors(A){
if(!A || A.length<2) return '';
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`;
}
return d;
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Bearbeiten';
edit.addEventListener('click',()=>{ selectStroke(s); editBezierCb.checked = true; drawBezierUI(); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${s.isBezier? 'Bezier' : (s.points?.length||0)+' pts'}`;
li.append('Stroke', sel, sw, edit, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx]; if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
if(selectedStroke===s){ selectedStroke=null; clearBezierUI(); }
strokes.splice(idx,1);
listRefresh();
}
function selectStroke(s){
selectedStroke = s;
strokes.forEach(st=> st.pathEl.classList.toggle('selected', st===s));
}
// -------- Freehand --------
function toPathD(s){
if(s.isBezier) return pathFromAnchors(s.anchors);
const pts = s.points;
const smp = s.sampler || samplerSel.value;
if(pts.length===0) return '';
if(smp==='raw'){
let d = `M ${pts[0].x} ${pts[0].y}`;
for(let i=1;i<pts.length;i++){ d+=` L ${pts[i].x} ${pts[i].y}`; } return d;
} else if(smp==='cubic'){
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const P=(i)=>pts[ clamp(i,0,pts.length-1) ];
let d=`M ${pts[0].x} ${pts[0].y}`;
for(let i=0;i<pts.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1x = p1.x + (p2.x - p0.x)/6, c1y = p1.y + (p2.y - p0.y)/6;
const c2x = p2.x - (p3.x - p1.x)/6, c2y = p2.y - (p3.y - p1.y)/6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
} return d;
} else {
let d = `M ${pts[0].x} ${pts[0].y}`;
for(let i=1;i<pts.length-1;i++){ const p0=pts[i], p1=pts[i+1]; const mx=(p0.x+p1.x)/2, my=(p0.y+p1.y)/2; d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`; }
const last=pts[pts.length-1]; d += ` L ${last.x} ${last.y}`; return d;
}
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function startStroke(e){
if(penActive || editBezierCb.checked){ return; }
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
const t = now();
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[pt], pathEl, start:t, duration:0, length:0, sampler:samplerSel.value, isBezier:false };
applyStrokeStyle(curStroke); updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
drawing = true;
}
stage.addEventListener('pointermove', (e)=>{
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push(p);
updatePath(curStroke);
}
});
window.addEventListener('pointerup', ()=>{
if(drawing && curStroke){
const pts = curStroke.points;
curStroke.duration = 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
drawing=false;
});
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function placeSpark(e){
const p = svgPoint(e);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
// Editor helpers
function clearBezierUI(){ bezierUI.innerHTML=''; selectedAnchor={stroke:null, idx:-1}; delAnchorBtn.disabled = true; }
function drawBezierUI(){
clearBezierUI();
const s = selectedStroke;
if(!editBezierCb.checked || !s || !s.isBezier) return;
s.anchors.forEach((a,idx)=>{
bezierUI.append(line(a.x, a.y, a.h1.x, a.h1.y, 'handleLine'));
bezierUI.append(line(a.x, a.y, a.h2.x, a.h2.y, 'handleLine'));
const h1 = circle(a.h1.x, a.h1.y, 5, 'handle', (e)=>startDrag('h1', s, idx, e));
const h2 = circle(a.h2.x, a.h2.y, 5, 'handle', (e)=>startDrag('h2', s, idx, e));
bezierUI.append(h1, h2);
const an = circle(a.x, a.y, 6.5, 'anchor', (e)=>{ startDrag('anchor', s, idx, e) });
an.addEventListener('dblclick', (e)=>{ e.stopPropagation(); insertAnchorAt(s, idx); });
an.addEventListener('pointerdown', ()=>{ selectedAnchor={stroke:s,idx}; delAnchorBtn.disabled=false; });
bezierUI.append(an);
});
}
function line(x1,y1,x2,y2,cls){ const l = document.createElementNS('http://www.w3.org/2000/svg','line'); l.setAttribute('x1',x1); l.setAttribute('y1',y1); l.setAttribute('x2',x2); l.setAttribute('y2',y2); l.setAttribute('class',cls); return l; }
function circle(x,y,r,cls,onDown){
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class',cls);
c.style.touchAction = 'none';
if(onDown) c.addEventListener('pointerdown', (e)=>{ e.stopPropagation(); onDown(e); });
return c;
}
function startDrag(type, s, idx, ev){
ev.preventDefault();
const a = s.anchors[idx];
const p0 = svgPoint(ev);
const dragging = {type, stroke:s, idx, ox:p0.x, oy:p0.y, pid:ev.pointerId};
try { stage.setPointerCapture(ev.pointerId); } catch(e){}
const move = (e)=>{
const pos = svgPoint(e);
const dx = pos.x - dragging.ox, dy = pos.y - dragging.oy;
dragging.ox = pos.x; dragging.oy = pos.y;
if(type==='anchor'){
a.x+=dx; a.y+=dy; a.h1.x+=dx; a.h1.y+=dy; a.h2.x+=dx; a.h2.y+=dy;
}else if(type==='h1'){
a.h1.x+=dx; a.h1.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h2.x = a.x - (a.h1.x - a.x);
a.h2.y = a.y - (a.h1.y - a.y);
}
}else if(type==='h2'){
a.h2.x+=dx; a.h2.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h1.x = a.x - (a.h2.x - a.x);
a.h1.y = a.y - (a.h2.y - a.y);
}
}
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
};
const up = ()=>{
try { stage.releasePointerCapture(ev.pointerId); } catch(_) {}
window.removeEventListener('pointermove', move);
};
window.addEventListener('pointermove', move, {passive:true});
window.addEventListener('pointerup', up, {once:true});
}
function insertAnchorAt(s, idx){
const A = s.anchors; if(idx>=A.length-1) return;
const a=A[idx], b=A[idx+1];
const mid = {x:(a.x+b.x)/2, y:(a.y+b.y)/2};
const dir = {x:(b.x-a.x), y:(b.y-a.y)};
const hLen = {x:dir.x*0.33, y:dir.y*0.33};
const newA = { x: mid.x, y: mid.y,
h1:{x: mid.x - hLen.x*0.5, y: mid.y - hLen.y*0.5},
h2:{x: mid.x + hLen.x*0.5, y: mid.y + hLen.y*0.5},
split:false };
A.splice(idx+1,0,newA);
s.pathEl.setAttribute('d', pathFromAnchors(A));
drawBezierUI();
}
delAnchorBtn.addEventListener('click', ()=>{
const s=selectedStroke, idx=selectedAnchor.idx;
if(!s || idx<0) return;
if(s.anchors.length<=2) return;
s.anchors.splice(idx,1);
selectedAnchor={stroke:null,idx:-1};
delAnchorBtn.disabled=true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
});
toBezierBtn.addEventListener('click', ()=>{
if(selectedStroke && !selectedStroke.isBezier){
const s=selectedStroke; const pts=s.points;
if(!pts || pts.length<2) return;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const P=(i)=>pts[ clamp(i,0,pts.length-1) ];
const A=[];
for(let i=0;i<pts.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1 = {x: p1.x + (p2.x - p0.x)/6, y: p1.y + (p2.y - p0.y)/6};
const c2 = {x: p2.x - (p3.x - p1.x)/6, y: p2.y - (p3.y - p1.y)/6};
if(i===0){ A.push({x:p1.x,y:p1.y,h1:{x:p1.x,y:p1.y},h2:{x:c1.x,y:c1.y},split:false}); }
else { A[A.length-1].h2 = {x:c1.x,y:c1.y}; }
A.push({x:p2.x,y:p2.y,h1:{x:c2.x,y:c2.y},h2:{x:p2.x,y:p2.y},split:false});
}
s.anchors=A; s.isBezier=true;
s.pathEl.setAttribute('d', pathFromAnchors(A));
listRefresh(); drawBezierUI();
}
});
splitBtn.addEventListener('click', ()=>{
if(selectedStroke && selectedAnchor.idx>=0){ const a=selectedStroke.anchors[selectedAnchor.idx]; a.split=!a.split; drawBezierUI(); }
});
window.addEventListener('keydown', (e)=>{
if((e.key==='Delete'||e.key==='Backspace') && penActive){ penBackspace(); }
if((e.key==='Delete'||e.key==='Backspace') && editBezierCb.checked){ delAnchorBtn.click(); }
if(e.key==='l' || e.key==='L'){ linkHandlesCb.checked = !linkHandlesCb.checked; }
if(e.key==='Enter' && penActive){ finishPen(); }
});
// Pen tool
penModeCb.addEventListener('change', ()=>{
const on = penModeCb.checked;
if(on){ editBezierCb.checked=false; penActive=true; ghost.setAttribute('d',''); deselectAll(); }
else { penActive=false; cancelPen(); }
finishPenBtn.disabled = !on;
});
finishPenBtn.addEventListener('click', finishPen);
function startPen(e){
const p = svgPoint(e);
if(!penStroke){
penStroke = { id: Date.now()%1e7, mode, width:+strokeInp.value, colorA: colorAInp.value, colorB: colorBInp.value,
isBezier:true, anchors:[], pathEl:createPathEl() };
applyStrokeStyle(penStroke);
const a = {x:p.x,y:p.y,h1:{x:p.x,y:p.y},h2:{x:p.x,y:p.y},split:false};
penStroke.anchors.push(a);
placingFirst=true; penDragging=true; penStartPt=p;
try{ stage.setPointerCapture(e.pointerId); }catch(_){}}
else {
const a = {x:p.x,y:p.y,h1:{x:p.x,y:p.y},h2:{x:p.x,y:p.y},split:false};
penStroke.anchors.push(a);
placingNext=true; penDragging=true;
try{ stage.setPointerCapture(e.pointerId); }catch(_){}}
}
function movePen(e){
if(!penStroke || !penDragging) return;
const p = svgPoint(e);
const A = penStroke.anchors;
if(placingFirst){
const start=A[0];
let v = {x: p.x - start.x, y: p.y - start.y};
if(e.shiftKey){
const ang=Math.atan2(v.y,v.x), step=Math.PI/12; const a=Math.round(ang/step)*step; const L=Math.hypot(v.x,v.y); v={x:Math.cos(a)*L,y:Math.sin(a)*L};
}
start.h2 = {x:start.x+v.x, y:start.y+v.y};
} else if(placingNext){
const last=A[A.length-1], prev=A[A.length-2];
let v = {x: p.x - last.x, y: p.y - last.y};
if(e.shiftKey){
const ang=Math.atan2(v.y,v.x), step=Math.PI/12; const a=Math.round(ang/step)*step; const L=Math.hypot(v.x,v.y); v={x:Math.cos(a)*L,y:Math.sin(a)*L};
}
last.h1 = {x:last.x+v.x, y:last.y+v.y};
if(e.altKey){ last.split=true; last.h2={x:last.x,y:last.y}; }
else { last.split=false; last.h2 = {x:last.x - v.x, y:last.y - v.y}; }
// previous outgoing 1/3 along segment
const seg = {x:last.x - prev.x, y:last.y - prev.y};
const L = Math.hypot(seg.x,seg.y); if(L>0){
const n={x:seg.x/L,y:seg.y/L}; const k=L/3;
prev.h2 = {x: prev.x + n.x*k, y: prev.y + n.y*k};
}
}
ghost.setAttribute('d', pathFromAnchors(A));
penStroke.pathEl.setAttribute('d', pathFromAnchors(A));
applyStrokeStyle(penStroke);
penStroke.pathEl.setAttribute('stroke-width', penStroke.width);
}
function endPen(e){ penDragging=false; placingFirst=false; placingNext=false; ghost.setAttribute('d',''); }
function finishPen(){ if(!penStroke) return; strokes.push(penStroke); listRefresh(); penStroke=null; penStartPt=null; ghost.setAttribute('d',''); }
function cancelPen(){ if(penStroke){ penStroke.pathEl.remove(); } penStroke=null; penStartPt=null; ghost.setAttribute('d',''); }
function penBackspace(){ if(!penStroke) return; const A=penStroke.anchors; if(A.length<=1){ cancelPen(); } else { A.pop(); penStroke.pathEl.setAttribute('d', pathFromAnchors(A)); } }
// Stage routing
stage.addEventListener('pointerdown', (e)=>{ if(penModeCb.checked) startPen(e); else startStroke(e); });
stage.addEventListener('pointermove', (e)=>{ if(penModeCb.checked) movePen(e); });
window.addEventListener('pointerup', (e)=>{ if(penModeCb.checked) endPen(e); });
// Misc
function exportSVGString(){
frameRect.style.display = frameOnCb.checked ? 'block' : 'none';
const clone = stage.cloneNode(true);
const ui = clone.querySelector('#bezierUI'); if(ui) ui.remove();
const gh = clone.querySelector('#ghost'); if(gh) gh.remove();
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>\n${s}`;
}
function motionJSON(){
const items = strokes.map(s=>{
const o = { id:s.id, mode:s.mode, colorA:s.colorA, colorB:s.colorB, width:s.width, sampler:s.sampler || 'cubic' };
if(s.isBezier){
o.isBezier = true;
o.anchors = s.anchors.map(a=>({x:a.x,y:a.y,h1:{x:a.h1.x,y:a.h1.y},h2:{x:a.h2.x,y:a.h2.y},split:!!a.split}));
}else{
o.points = (s.points||[]).map(p=>({x:p.x,y:p.y}));
}
return o;
});
return JSON.stringify({strokes:items}, null, 2);
}
document.getElementById('copySVG').addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
document.getElementById('downloadSVG').addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
document.getElementById('downloadPNG').addEventListener('click', ()=>{ const svg=exportSVGString(); svgToPng(svg, 'omi-omega-oneliner.png'); });
document.getElementById('copyJSON').addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
document.getElementById('downloadJSON').addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
document.getElementById('replay').addEventListener('click', ()=>{
if(!strokes.length) return;
strokes.forEach(s=> s.pathEl.setAttribute('d',''));
const total=1500; const t0=performance.now();
const step=()=>{
const t=performance.now()-t0;
strokes.forEach(s=>{
if(!s.isBezier) return;
const A=s.anchors, n=A.length-1; const k=Math.max(1, Math.floor((t/total)*n));
let d=`M ${A[0].x} ${A[0].y}`;
for(let i=0;i<k && i<n;i++){ const a=A[i], b=A[i+1]; d+=` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`; }
s.pathEl.setAttribute('d', d); applyStrokeStyle(s); s.pathEl.setAttribute('stroke-width', s.width);
});
if(t<total) requestAnimationFrame(step); else { strokes.forEach(updatePath); }
};
requestAnimationFrame(step);
});
function svgToPng(svgString, filename){
const img = new Image();
const svgBlob = new Blob([svgString], {type:'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function(){
const canvas = document.createElement('canvas');
const vb = stage.viewBox.baseVal;
canvas.width = vb.width; canvas.height = vb.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob)=>{
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click();
});
URL.revokeObjectURL(url);
};
img.src = url;
}
// Basic toggles
frameRect.style.display = document.getElementById('frameOn').checked ? 'block' : 'none';
document.getElementById('frameOn').addEventListener('change', (e)=>{ frameRect.style.display = e.target.checked ? 'block' : 'none'; });
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{ strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; ghost.setAttribute('d',''); clearBezierUI(); listRefresh(); });
function deselectAll(){ selectedStroke=null; strokes.forEach(st=> st.pathEl.classList.remove('selected')); clearBezierUI(); }
})();</script>
</body>
</html>

View File

@@ -0,0 +1,452 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Schnippsi Painter — Stream + Bezier</title>
<style>
:root{
--bg:#0b0f12; --panel:#12181f; --ink:#e2f2ff; --muted:#7a8aa0; --accent:#62d3a4; --accent2:#ffd166;
}
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
#wrap{display:grid;grid-template-rows:auto 1fr; height:100%;}
#toolbar{
display:flex;gap:.75rem;align-items:center; padding:.6rem .8rem; background:linear-gradient(180deg,#12181f,#0e141a);
position:sticky; top:0; z-index:3; box-shadow:0 8px 20px rgba(0,0,0,.25);
}
#toolbar input[type=color], #toolbar input[type=text]{height:32px}
#toolbar .btn{
padding:.55rem .8rem; border-radius:14px; background:#1a2330; color:#cfe7ff; border:1px solid #2b3a4b; cursor:pointer;
}
#toolbar .btn[aria-pressed="true"]{background:linear-gradient(180deg,#1f2a36,#1a2330); outline:2px solid var(--accent);}
#toolbar .danger{border-color:#603; color:#ffd9e6; background:#2a0f1f}
#toolbar .ok{border-color:#1b5e3b; background:#0f241a; color:#d9ffef;}
#toolbar .field{display:flex;align-items:center;gap:.4rem}
#status{margin-left:auto; font-size:.9rem; color:var(--muted)}
#canvasWrap{position:relative; height:100%;}
canvas{position:absolute; inset:0; width:100%; height:100%; touch-action:none; background:#0a0d11; outline:none}
#help{
position:fixed; right:.8rem; bottom:.8rem; width:360px; max-width:95vw; background:#0e141a; border:1px solid #213041;
border-radius:16px; padding:12px 14px; color:#cfe7ff; box-shadow:0 18px 40px rgba(0,0,0,.45); font-size:14px;
}
#help h3{margin:.2rem 0 .4rem; font-size:16px}
#help kbd{background:#111a22; padding:.15rem .35rem; border-radius:6px; border:1px solid #2b3a4b}
.pill{padding:.08rem .45rem; background:#13202a; border:1px solid #254257; border-radius:999px; font-size:12px; color:#bfe8ff}
.handle{cursor:grab}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
</style>
</head>
<body>
<div id="wrap">
<div id="toolbar" role="toolbar" aria-label="Zeichenwerkzeuge">
<div class="field"><span class="pill">Tool</span>
<button class="btn" id="toolFree" aria-pressed="true" aria-label="Freihand (F)">Freihand</button>
<button class="btn" id="toolBezier" aria-pressed="false" aria-label="Bezier (B)">Bezier</button>
</div>
<div class="field"><span class="pill">Farbe A</span><input id="colA" type="color" value="#62d3a4"/></div>
<div class="field"><span class="pill">Farbe B</span><input id="colB" type="color" value="#ffd166"/></div>
<div class="field"><span class="pill">Breite</span><input id="width" type="range" min="1" max="48" value="12"/></div>
<div class="field"><span class="pill">Stream</span>
<input id="wsUrl" type="text" size="22" value="ws://localhost:9980" aria-label="WebSocket URL"/>
<button class="btn" id="wsToggle" aria-pressed="false" aria-label="Streaming an/aus (S)">Verbinden</button>
</div>
<div class="field">
<button class="btn" id="replayBtn" aria-label="Replay (R)">Replay</button>
<button class="btn" id="exportJson">JSON</button>
<button class="btn" id="exportSvg">SVG</button>
<button class="btn" id="exportPng">PNG</button>
<button class="btn danger" id="clearBtn">Clear</button>
</div>
<div id="status" aria-live="polite">bereit</div>
</div>
<div id="canvasWrap">
<canvas id="cv" role="application" aria-label="Schnippsi Zeichenfläche" tabindex="0"></canvas>
</div>
</div>
<aside id="help" aria-live="polite">
<h3>Shortcuts</h3>
<div>Hilfe <kbd>?</kbd> / <kbd>H</kbd> • Tool-Bezier <kbd>B</kbd> • Tool-Freihand <kbd>F</kbd> • Stream <kbd>S</kbd></div>
<div>Bezier beenden <kbd>Enter</kbd> • Letzten Anker löschen <kbd>Backspace</kbd> • Ecke halten <kbd>Alt</kbd> • Snap <kbd>Shift</kbd></div>
<div class="pill">A11y</div> Tab-fokussierbar, ARIA Live-Status, hoher Kontrast, 200% Zoom stabil.
</aside>
<script>
(()=>{
const dpr = Math.max(1, window.devicePixelRatio || 1);
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const toolFree = document.getElementById('toolFree');
const toolBezier = document.getElementById('toolBezier');
const colA = document.getElementById('colA');
const colB = document.getElementById('colB');
const widthIn = document.getElementById('width');
const replayBtn = document.getElementById('replayBtn');
const exportJson = document.getElementById('exportJson');
const exportSvg = document.getElementById('exportSvg');
const exportPng = document.getElementById('exportPng');
const clearBtn = document.getElementById('clearBtn');
const wsToggle = document.getElementById('wsToggle');
const wsUrl = document.getElementById('wsUrl');
let state = {
tool: 'free', // 'free' | 'bezier'
colorA: colA.value,
colorB: colB.value,
width: +widthIn.value,
strokes: [],
current: null,
sessionId: ('sess_' + Math.random().toString(16).slice(2)),
streaming: false,
ws: null,
down: false,
modifiers: {shift:false, alt:false},
bezier: {anchors:[], dragging:null}
};
function resize(){
const rect = cv.getBoundingClientRect();
cv.width = Math.floor(rect.width * dpr);
cv.height = Math.floor(rect.height * dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
render();
}
new ResizeObserver(resize).observe(document.getElementById('canvasWrap'));
resize();
function setStatus(t){ statusEl.textContent = t; }
// WebSocket
function wsConnect(){
try{
const url = wsUrl.value.trim();
state.ws = new WebSocket(url);
state.ws.onopen = ()=>{ state.streaming = true; wsToggle.setAttribute('aria-pressed','true'); setStatus('WS verbunden'); send({type:'session', id:state.sessionId, app:'schnippsi-painter', t:performance.now()|0}); };
state.ws.onclose = ()=>{ state.streaming = false; wsToggle.setAttribute('aria-pressed','false'); setStatus('WS getrennt'); };
state.ws.onerror = ()=>{ setStatus('WS Fehler'); };
wsToggle.textContent = 'Trennen';
}catch(e){ console.error(e); setStatus('WS Verbindungsfehler'); }
}
function wsDisconnect(){
if(state.ws){ try{ state.ws.close(); }catch(e){} }
state.ws = null; state.streaming = false; wsToggle.textContent = 'Verbinden';
}
function send(obj){
if(!state.streaming || !state.ws || state.ws.readyState!==1) return;
try{ state.ws.send(JSON.stringify(obj)); }catch(e){ /* ignore */ }
}
// Tools
function useTool(t){
state.tool = t;
toolFree.setAttribute('aria-pressed', t==='free');
toolBezier.setAttribute('aria-pressed', t==='bezier');
setStatus(t==='free'?'Freihand aktiv':'Bezier aktiv');
}
toolFree.onclick = ()=>useTool('free');
toolBezier.onclick = ()=>useTool('bezier');
colA.oninput = ()=>{ state.colorA = colA.value; };
colB.oninput = ()=>{ state.colorB = colB.value; };
widthIn.oninput = ()=>{ state.width = +widthIn.value; };
wsToggle.onclick = ()=>{
if(state.streaming) wsDisconnect();
else wsConnect();
};
clearBtn.onclick = ()=>{ state.strokes.length=0; state.bezier.anchors=[]; state.current=null; render(); };
// Geometry helpers
const dist = (a,b)=>Math.hypot(a.x-b.x, a.y-b.y);
function lerp(a,b,t){ return a+(b-a)*t; }
function cubicPoint(p0,p1,p2,p3,t){
const x = Math.pow(1-t,3)*p0.x + 3*Math.pow(1-t,2)*t*p1.x + 3*(1-t)*t*t*p2.x + t*t*t*p3.x;
const y = Math.pow(1-t,3)*p0.y + 3*Math.pow(1-t,2)*t*p1.y + 3*(1-t)*t*t*p2.y + t*t*t*p3.y;
return {x,y};
}
// Drawing primitives
function drawStroke(s){
ctx.lineJoin = ctx.lineCap = 'round';
ctx.lineWidth = s.width;
ctx.strokeStyle = s.colorA;
ctx.globalAlpha = 1;
ctx.beginPath();
if(s.isBezier){
const A = s.anchors;
if(A.length>0){ ctx.moveTo(A[0].x, A[0].y); }
for(let i=1;i<A.length;i++){
const a0=A[i-1], a1=A[i];
ctx.bezierCurveTo(a0.h2.x, a0.h2.y, a1.h1.x, a1.h1.y, a1.x, a1.y);
}
ctx.stroke();
// Handles (edit mode)
if(s._edit){
for(const a of A){
// anchor
ctx.fillStyle = '#cfe7ff';
ctx.beginPath(); ctx.arc(a.x,a.y,4,0,Math.PI*2); ctx.fill();
// handles
ctx.strokeStyle='#2b9ad1'; ctx.lineWidth=1.2;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(a.h1.x,a.h1.y); ctx.moveTo(a.x,a.y); ctx.lineTo(a.h2.x,a.h2.y); ctx.stroke();
ctx.fillStyle='#2b9ad1';
ctx.beginPath(); ctx.arc(a.h1.x,a.h1.y,3,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(a.h2.x,a.h2.y,3,0,Math.PI*2); ctx.fill();
}
}
}else{
const P = s.points;
if(!P || P.length<2) return;
ctx.moveTo(P[0].x, P[0].y);
for(let i=1;i<P.length;i++){ ctx.lineTo(P[i].x, P[i].y); }
ctx.stroke();
}
}
function render(){
ctx.clearRect(0,0,cv.width,dpr?cv.height:cv.height);
// backdrop grid (subtle)
ctx.save();
ctx.strokeStyle='rgba(255,255,255,0.04)';
ctx.lineWidth=1;
for(let x=0;x<cv.width/dpr;x+=40){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,cv.height/dpr); ctx.stroke(); }
for(let y=0;y<cv.height/dpr;y+=40){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(cv.width/dpr,y); ctx.stroke(); }
ctx.restore();
for(const s of state.strokes){ drawStroke(s); }
if(state.current){ drawStroke(state.current); }
}
// Input
let lastEmit = 0;
function pointerPos(e){
const r = cv.getBoundingClientRect();
return { x:(e.clientX - r.left), y:(e.clientY - r.top), t: performance.now()|0, p:e.pressure||0.5, tilt:[e.tiltX||0, e.tiltY||0], type:e.pointerType||'mouse' };
}
function startStroke(pt){
if(state.tool==='free'){
state.current = { id:('st_'+Date.now()), tool:'free', colorA:state.colorA, colorB:state.colorB, width:state.width, points:[pt], isBezier:false };
send({type:'strokeStart', id:state.current.id, tool:'free', colorA:state.colorA, colorB:state.colorB, width:state.width, t:pt.t});
}else{
// initialize bezier edit stroke
const A = state.bezier.anchors.length? structuredClone(state.bezier.anchors) : [];
state.current = { id:('st_'+Date.now()), tool:'bezier', colorA:state.colorA, colorB:state.colorB, width:state.width, anchors:A, isBezier:true, _edit:true };
if(A.length===0){
// first anchor with mirrored handles
const a = {x:pt.x,y:pt.y, h1:{x:pt.x-40, y:pt.y}, h2:{x:pt.x+40, y:pt.y}, split:false, t:pt.t, p:pt.p, tilt:pt.tilt};
state.bezier.anchors.push(a);
state.current.anchors.push(structuredClone(a));
send({type:'strokeStart', id:state.current.id, tool:'bezier', colorA:state.colorA, colorB:state.colorB, width:state.width, t:pt.t});
send({type:'anchor', id:state.current.id, a:a});
}
}
}
function moveStroke(pt){
if(!state.current) return;
if(state.tool==='free'){
state.current.points.push(pt);
const now = performance.now();
if(now-lastEmit>33){ lastEmit=now; send({type:'point', id:state.current.id, p:pt}); }
}else{
// handle dragging logic
const cur = state.current;
if(!state.bezier.dragging) return;
const drag = state.bezier.dragging;
if(drag.kind==='anchor'){
drag.ref.x = pt.x; drag.ref.y = pt.y;
// move handles with anchor
drag.ref.h1.x += drag.dx? (pt.x-drag.base.x) : 0;
drag.ref.h1.y += drag.dy? (pt.y-drag.base.y) : 0;
drag.ref.h2.x += drag.dx? (pt.x-drag.base.x) : 0;
drag.ref.h2.y += drag.dy? (pt.y-drag.base.y) : 0;
drag.base.x = pt.x; drag.base.y = pt.y;
}else if(drag.kind==='h1' || drag.kind==='h2'){
drag.ref[drag.kind].x = pt.x; drag.ref[drag.kind].y = pt.y;
if(!drag.ref.split){
// mirror handle lengths for smooth
const ax=drag.ref.x, ay=drag.ref.y;
const opp = (drag.kind==='h1')?'h2':'h1';
const vx = ax - pt.x, vy = ay - pt.y;
drag.ref[opp].x = ax + vx; drag.ref[opp].y = ay + vy;
}
}
cur.anchors = structuredClone(state.bezier.anchors);
// emit throttle
const now = performance.now();
if(now-lastEmit>50){ lastEmit=now; send({type:'anchors', id:cur.id, anchors:cur.anchors}); }
}
render();
}
function endStroke(pt){
if(!state.current) return;
if(state.tool==='free'){
state.strokes.push(state.current);
send({type:'strokeEnd', id:state.current.id, t:pt.t});
state.current=null;
}else{
// in bezier tool, mouseup only stops dragging
state.bezier.dragging=null;
}
render();
}
// Hit-testing for bezier edit
function hitTestAnchors(x,y){
const A = state.bezier.anchors;
for(let i=A.length-1;i>=0;i--){
const a=A[i];
if(dist({x,y}, a)<8) return {idx:i, kind:'anchor', ref:a};
if(dist({x,y}, a.h1)<7) return {idx:i, kind:'h1', ref:a};
if(dist({x,y}, a.h2)<7) return {idx:i, kind:'h2', ref:a};
}
return null;
}
// Add anchor with dragging of handle based on movement
function addAnchor(pt){
const A = state.bezier.anchors;
const prev = A[A.length-1];
const a = {x:pt.x,y:pt.y, h1:{x:pt.x-40, y:pt.y}, h2:{x:pt.x+40, y:pt.y}, split:false, t:pt.t, p:pt.p, tilt:pt.tilt};
if(prev){
// make smooth by default: h1 points to previous, h2 mirrored
a.h1 = {x: lerp(prev.x, pt.x, .33), y: lerp(prev.y, pt.y, .33)};
a.h2 = {x: lerp(pt.x, prev.x, .33), y: lerp(pt.y, prev.y, .33)};
}
A.push(a);
if(state.current){ state.current.anchors = structuredClone(A); }
send({type:'anchor', id:state.current?state.current.id:('st_'+Date.now()), a:a});
render();
}
// Pointer events
cv.addEventListener('pointerdown', (e)=>{
cv.setPointerCapture(e.pointerId);
const pt = pointerPos(e); state.down=true;
if(state.tool==='free'){
startStroke(pt);
}else{
// bezier: check hit on existing handle/anchor first
const hit = hitTestAnchors(pt.x,pt.y);
if(hit){
state.bezier.dragging={...hit, base:{x:pt.x,y:pt.y}, dx:true, dy:true};
if(hit.kind!=='anchor' && e.altKey){ hit.ref.split = true; } // allow cusp with Alt while grabbing
}else{
if(!state.current){ startStroke(pt); }
addAnchor(pt);
}
}
render();
});
cv.addEventListener('pointermove', (e)=>{
if(!state.down && state.tool==='bezier'){
// hover cursor
const pt = pointerPos(e), hit = hitTestAnchors(pt.x,pt.y);
cv.style.cursor = hit ? 'grab' : 'crosshair';
}
if(!state.down) return;
moveStroke(pointerPos(e));
});
cv.addEventListener('pointerup', (e)=>{ state.down=false; endStroke(pointerPos(e)); });
cv.addEventListener('pointercancel', (e)=>{ state.down=false; endStroke(pointerPos(e)); });
// Keyboard
window.addEventListener('keydown', (e)=>{
if(e.key==='Shift') state.modifiers.shift=true;
if(e.key==='Alt') state.modifiers.alt=true;
if(e.key==='b' || e.key==='B'){ useTool('bezier'); }
if(e.key==='f' || e.key==='F'){ useTool('free'); }
if(e.key==='s' || e.key==='S'){ state.streaming?wsDisconnect():wsConnect(); }
if(e.key==='r' || e.key==='R'){ doReplay(); }
if(e.key==='Enter' && state.tool==='bezier'){
// finalize bezier stroke into strokes
if(state.current){
const finalized = {...state.current};
finalized._edit=false;
state.strokes.push(finalized);
send({type:'strokeEnd', id:finalized.id, t:performance.now()|0});
state.current=null;
state.bezier.anchors = [];
render();
}
}
if(e.key==='Backspace' && state.tool==='bezier'){
if(state.bezier.anchors.length>0){
const a = state.bezier.anchors.pop();
send({type:'anchorDel', id:state.current?state.current.id:null, aidx: state.bezier.anchors.length});
if(state.current) state.current.anchors = structuredClone(state.bezier.anchors);
render();
}
e.preventDefault();
}
if(e.key==='?' || e.key==='h' || e.key==='H'){
const el=document.getElementById('help'); el.hidden = !el.hidden;
}
});
window.addEventListener('keyup', (e)=>{
if(e.key==='Shift') state.modifiers.shift=false;
if(e.key==='Alt') state.modifiers.alt=false;
});
// Export
function download(name, dataUrl){
const a=document.createElement('a'); a.href=dataUrl; a.download=name; a.click();
}
exportJson.onclick = ()=>{
const payload = {
canvas:{w:cv.width/dpr, h:cv.height/dpr, dpr},
session:{id:state.sessionId, app:'schnippsi-painter'},
strokes: state.strokes
};
const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'});
download(`schnippsi_${Date.now()}.json`, URL.createObjectURL(blob));
};
exportSvg.onclick = ()=>{
const w=cv.width/dpr, h=cv.height/dpr;
function pathFromStroke(s){
if(s.isBezier){
const A=s.anchors; if(A.length<1) return '';
let d=`M ${A[0].x} ${A[0].y}`;
for(let i=1;i<A.length;i++){
const a0=A[i-1], a1=A[i];
d+=` C ${a0.h2.x} ${a0.h2.y}, ${a1.h1.x} ${a1.h1.y}, ${a1.x} ${a1.y}`;
}
return `<path d="${d}" stroke="${s.colorA}" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="${s.width}"/>`;
}else{
const P=s.points; if(!P || P.length<2) return '';
const d='M '+P.map((p,i)=> (i? 'L '+p.x+' '+p.y : p.x+' '+p.y)).join(' ');
return `<path d="${d}" stroke="${s.colorA}" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="${s.width}"/>`;
}
}
const body = state.strokes.map(pathFromStroke).join('\n');
const svg = `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n${body}\n</svg>`;
const blob = new Blob([svg], {type:'image/svg+xml'});
download(`schnippsi_${Date.now()}.svg`, URL.createObjectURL(blob));
};
exportPng.onclick = ()=>{
const url = cv.toDataURL('image/png');
download(`schnippsi_${Date.now()}.png`, url);
};
// Replay
function doReplay(){
if(state.strokes.length===0) return;
const copy = JSON.parse(JSON.stringify(state.strokes));
state.strokes = [];
let idx=0;
function step(){
if(idx>=copy.length) return;
state.strokes.push(copy[idx++]); render();
setTimeout(step, 300);
}
step();
}
replayBtn.onclick = doReplay;
// Initial tip
setStatus('Bezier: Klicken für Anker, ziehen für Griffe. Enter = fertig. Alt zum Brechen, Shift für Snap.');
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Crumbblocks Wasserkocher (Demo)</title>
<!-- Feste Blockly-Version (verhindert textToDom-Fehler) -->
<script src="https://unpkg.com/blockly@9.3.3/blockly.min.js"></script>
<script src="https://unpkg.com/blockly@9.3.3/msg/de.js"></script>
<style>
:root {
--bg: #0b1016;
--panel: #0e141b;
--ink: #e6edf3;
--muted: #93a1b3;
--accent: #4caf50;
--border: #1f2a37;
--console: #0a0f14;
--console-ink: #b7ffb2;
}
html, body {
height: 100%;
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
.wrap {
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 12px;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
header h1 { font-size: 20px; margin: 0; }
.controls {
display: flex; gap: 8px; flex-wrap: wrap;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px; padding: 10px;
}
button {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 8px;
padding: 10px 14px;
font-weight: 700;
cursor: pointer;
}
button.secondary { background: #374151; }
button:active { transform: translateY(1px); }
.stage {
display: grid; grid-template-rows: 64vh 28vh; gap: 12px;
}
#blocklyDiv {
width: 100%;
height: 64vh;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
#output {
width: 100%;
height: 28vh;
background: var(--console);
color: var(--console-ink);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
}
.hint {
color: var(--muted);
font-size: 12px;
margin-top: 6px;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>🧁 Crumbblocks Virtueller Wasserkocher (Kids-Demo)</h1>
<div class="hint">
Variablen: <code>power_w</code>, <code>wasser_ml</code>, <code>sensor_temp_c</code>, <code>zeit_s</code>,
<code>energie_wh</code>, <code>verbrauch_wh</code>. &nbsp; Ziel: Bis 100 °C erhitzen, Energie/Zeit mitrechnen.
</div>
</header>
<div class="controls">
<button onclick="loadDemo()">🎬 Demo laden</button>
<button onclick="runCode()">▶️ Ausführen</button>
<button class="secondary" onclick="clearOutput()">🧹 Leeren</button>
<span class="hint">Sendet per <code>POST /crumbapi/blockly-terminal</code> (Fallback: Echo).</span>
</div>
<div class="stage">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Terminal wartet …</pre>
</div>
<!-- TOOLBOX (Standardkategorien + Variablen/Funktionen) -->
<xml id="toolbox" style="display:none">
<category name="Logik" categorystyle="logic_category">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_boolean"></block>
</category>
<category name="Schleifen" categorystyle="loop_category">
<block type="controls_repeat_ext"><value name="TIMES"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="controls_whileUntil"><field name="MODE">WHILE</field></block>
<block type="controls_flow_statements"></block>
</category>
<category name="Mathe" categorystyle="math_category">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_round"></block>
</category>
<category name="Text" categorystyle="text_category">
<block type="text"></block>
<block type="text_print"></block>
</category>
<sep></sep>
<category name="Variablen" categorystyle="variable_category" custom="VARIABLE_DYNAMIC"></category>
<category name="Funktionen" categorystyle="procedure_category" custom="PROCEDURE"></category>
</xml>
<!-- DEMO-WORKSPACE (Wasserkocher) -->
<script id="demo-xml" type="text/plain">
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
</variables>
<!-- Startwerte -->
<block type="variables_set" x="24" y="24">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">250</field></block></value>
<next>
<!-- Sicherheitslimit Wasser -->
<block type="controls_if">
<value name="IF0">
<block type="logic_compare">
<field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">wasser_ml</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">2000</field></block></value>
</block>
</value>
<statement name="DO0">
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
</block>
</statement>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<!-- while (sensor_temp_c < 100) -->
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<!-- Abbruch wenn kein Strom -->
<block type="controls_if">
<value name="IF0">
<block type="logic_compare">
<field name="OP">LTE</field>
<value name="A"><block type="variables_get"><field name="VAR">power_w</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">0</field></block></value>
</block>
</value>
<statement name="DO0">
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Kein Strom STOP</field></block></value>
<next>
<block type="controls_flow_statements"><field name="FLOW">BREAK</field></block>
</next>
</block>
</statement>
<next>
<!-- Heize-Schritt -->
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Heize</field></block></value>
<next>
<!-- sensor_temp_c += 5 -->
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<!-- zeit_s += 5 -->
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">zeit_s</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<!-- energie_wh += power_w * 5 / 3600 -->
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
<value name="B">
<block type="math_arithmetic">
<field name="OP">DIVIDE</field>
<value name="A">
<block type="math_arithmetic">
<field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">power_w</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<value name="B"><block type="math_number"><field name="NUM">3600</field></block></value>
</block>
</value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Fertig. STOP.</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>
</script>
<script>
// Workspace
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'geras',
grid: { spacing: 24, length: 3, colour: '#223045', snap: true },
zoom: { controls: true, wheel: true, startScale: 1.05, maxScale: 2.0, minScale: 0.5, pinch: true },
trashcan: true
});
window.addEventListener('resize', () => Blockly.svgResize(workspace));
function out(txt, replace=false) {
const el = document.getElementById('output');
el.textContent = replace ? String(txt) : (el.textContent + '\n' + String(txt));
el.scrollTop = el.scrollHeight;
}
function clearOutput() { document.getElementById('output').textContent = '🌲 Terminal wartet …'; }
function loadDemo() {
try {
const xmlTxt = document.getElementById('demo-xml').textContent.trim();
const dom = (Blockly.Xml && Blockly.Xml.textToDom)
? Blockly.Xml.textToDom(xmlTxt)
: Blockly.utils.xml.textToDom(xmlTxt);
workspace.clear();
Blockly.Xml.domToWorkspace(dom, workspace);
out('✅ Demo geladen.', true);
} catch (e) {
out('❌ Demo konnte nicht geladen werden: ' + e, true);
}
}
async function runCode() {
const code = Blockly.JavaScript.workspaceToCode(workspace);
out('📤 Sende an Terminal…\n\n' + code, true);
// Variablenliste (für spätere Auswertung im Backend hilfreich)
const knownVars = ['sensor_temp_c','power_w','wasser_ml','zeit_s','energie_wh','verbrauch_wh'];
try {
const res = await fetch('/crumbapi/blockly-terminal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'blockly', blockcode: code, vars: knownVars })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
out('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
} catch (err) {
// Fallback: Echo-JSON lokal anzeigen
const echo = {
ok: true,
mode: 'echo-fallback',
request: { mode: 'blockly', blockcode: code, vars: knownVars }
};
out('\n Endpoint nicht bereit. Fallback aktiv.\n\n' + JSON.stringify(echo, null, 2));
}
}
// Direkt beim Laden einmal die Demo hineinlegen:
loadDemo();
</script>
</div>
</body>
</html>

298
crumbblocks/index.html Normal file
View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Blockly to Crumbforest Terminal Bridge</title>
<!-- Blockly + JS Generator + Deutsche Labels -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root {
--bg: #1e1e1e;
--panel: #111;
--text: #e8e8e8;
--ok: #a6e22e;
--btn: #4caf50;
}
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); font-family: monospace; }
header { display:flex; gap:.5rem; align-items:center; padding:.5rem .75rem; }
header h2 { margin:0; font-weight:600; }
header .spacer { flex:1; }
button {
padding:.6rem .9rem; background: var(--btn); color:#fff; border:none; border-radius:8px; font-weight:700; cursor:pointer;
}
#wrap { display:flex; flex-direction:column; height: calc(100vh - 56px); }
#blocklyDiv { height: 64vh; width: 100%; }
#bottom { display:flex; gap:.75rem; padding:.5rem .75rem; }
#output {
flex:1; height: 32vh; background:var(--panel); color:var(--ok);
padding:10px; overflow:auto; white-space: pre-wrap; word-break: break-word; border-radius:8px;
}
#controls { display:flex; flex-direction:column; gap:.5rem; }
.muted { opacity:.7; }
</style>
</head>
<body>
<header>
<h2>🧁 Blockly Crumbforest Bridge</h2>
<div class="spacer"></div>
<button id="btnDemo" title="Mini-Demo laden">🧪 Demo</button>
<button id="btnClear" title="Workspace leeren">🗑️ Neu</button>
<button id="btnZoomIn" title="Zoom +">🔍+</button>
<button id="btnZoomOut" title="Zoom ">🔍-</button>
<button id="btnRun" title="Code ausführen / an Terminal senden">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="blocklyDiv"></div>
<div id="bottom">
<div id="controls">
<div class="muted">Variablen: energie_wh, verbrauch_wh, sensor_temp_c, power_w, wasser_ml, zeit_s</div>
<div class="muted">Endpoint: <code>/crumbapi/blockly-terminal</code> (Echo-Fallback aktiv)</div>
</div>
<pre id="output">🌲 Terminal wartet ...</pre>
</div>
</div>
<!-- Toolbox: wie bei dir, nur Loops aktiviert -->
<xml id="toolbox" style="display:none">
<block type="text"></block>
<block type="text_print"></block>
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="variables_set"></block>
<block type="variables_get"></block>
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="procedures_defnoreturn"></block>
<block type="procedures_callnoreturn"></block>
</xml>
<script>
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing: 24, length: 3, colour: '#484848', snap: true},
trashcan: true,
zoom: {startScale: 1.15, maxScale: 2.0, minScale: 0.6, controls: false, wheel: true},
move: {scrollbars: true, drag: true, wheel: true}
});
// Vordefinierte Variablen für euren „Prozess“-Wortschatz
['energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s']
.forEach(v => { try { workspace.createVariable(v); } catch(e){} });
// Helpers
const $ = sel => document.querySelector(sel);
function setOutput(txt){ $('#output').textContent = txt; }
function appendOutput(txt){
const el = $('#output');
el.textContent += (el.textContent.endsWith('\n')?'':'\n') + txt;
el.scrollTop = el.scrollHeight;
}
// Initial-Variablen aus „set“-Blöcken sammeln (für JSON-Vorschau)
function collectInitialVars() {
const vals = {};
const blocks = workspace.getAllBlocks(false);
for (const b of blocks) {
if (b.type === 'variables_set') {
const name = b.getFieldValue('VAR');
// Versuche simples Literal zu lesen (Zahl/Text)
const input = b.getInputTargetBlock('VALUE');
if (input) {
if (input.type === 'math_number') {
vals[name] = Number(input.getFieldValue('NUM'));
} else if (input.type === 'text') {
vals[name] = input.getFieldValue('TEXT') ?? '';
} else {
// Irgendwas anderes verschachtelt → nur markieren
vals[name] = '<expr>';
}
} else {
vals[name] = '<unset>';
}
}
}
return vals;
}
// Kompatibles XML-Parserchen (fix für „Blockly.Xml.textToDom is not a function“)
function textToDom(xmlText){
if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText);
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
// Aktionen
$('#btnZoomIn').onclick = ()=> Blockly.svgResize(workspace), workspace.zoomCenter(1);
$('#btnZoomOut').onclick = ()=> Blockly.svgResize(workspace), workspace.zoomCenter(-1);
$('#btnClear').onclick = () => {
workspace.clear();
setOutput('🧹 Workspace geleert.');
};
$('#btnDemo').onclick = () => {
const demoXML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
</variables>
<block type="variables_set" x="24" y="22">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">250</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<block type="text_print">
<value name="TEXT">
<block type="text">
<field name="TEXT">Heize...</field>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">zeit_s</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">2.8</field></block></value>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT">
<block type="text">
<field name="TEXT">Fertig. STOP.</field>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>`;
try {
const dom = textToDom(demoXML);
workspace.clear();
Blockly.Xml.domToWorkspace(dom, workspace);
setOutput('✅ Demo geladen. Passe die Logik an und drücke ▶️ Ausführen.');
} catch (e) {
setOutput('❌ Demo konnte nicht geladen werden:\n' + e);
}
};
$('#btnRun').onclick = async () => {
const code = Blockly.JavaScript.workspaceToCode(workspace);
const initVars = collectInitialVars();
setOutput("📤 Sende an Terminal...\n\n" + code);
const payload = {
mode: "blockly",
blockcode: code,
preview_vars: initVars
};
try {
const res = await fetch("/crumbapi/blockly-terminal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
let data;
if (res.ok) {
data = await res.json().catch(()=>({}));
appendOutput("\n\n✅ Antwort:\n" + JSON.stringify(data, null, 2));
} else {
appendOutput("\n\n Endpoint nicht bereit (HTTP " + res.status + "). Fallback aktiv.");
appendOutput("\n\n✅ Antwort (Fallback):\n" + JSON.stringify({ ok:true, mode:"echo-fallback", request:payload }, null, 2));
}
} catch (err) {
appendOutput("\n\n❌ Netzwerkfehler: " + err);
appendOutput("\n\n✅ Antwort (Fallback):\n" + JSON.stringify({ ok:true, mode:"echo-fallback", request:payload }, null, 2));
}
};
// Resizing
window.addEventListener('resize', () => Blockly.svgResize(workspace));
</script>
</body>
</html>

278
crumbblocks/index_v1.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Crumbblocks Blockly ↔ Crumbforest</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly + Python-Generator + Deutsch -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/python.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{
--bg:#0d0f12;--panel:#12151b;--ink:#e9edf5;--muted:#b7c0d0;--accent:#4caf50;--danger:#e74c3c;
--mono:ui-monospace, Menlo, Monaco, "Courier New", monospace;
}
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono)}
header{position:sticky;top:0;z-index:5;display:flex;gap:.6rem;align-items:center;padding:.6rem 1rem;background:#0b0d10cc;border-bottom:1px solid #1b212c}
header h2{margin:0;font-size:1rem;color:var(--muted)}
.spacer{flex:1 1 auto}
.btn{border:1px solid #263041;background:#161b22;color:var(--ink);border-radius:8px;padding:.5rem .75rem;cursor:pointer;font-weight:700}
.btn:hover{filter:brightness(1.1)}
.btn.primary{background:var(--accent);color:#0b130c;border-color:#2b6b2e}
.btn.danger{background:var(--danger);border-color:#b13a2e}
.btn.ghost{background:transparent}
#wrap{display:flex;flex-direction:column;gap:.6rem;height:calc(100% - 54px);padding:.6rem 1rem 1rem}
#blocklyDiv{height:62vh;width:100%;border-radius:10px;overflow:hidden;background:#0f1319;border:1px solid #1b212c}
#console{display:grid;grid-template-rows:auto 1fr;gap:.4rem;height:28vh;background:var(--panel);border:1px solid #1b212c;border-radius:10px}
#consoleHead{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border-bottom:1px solid #1b212c}
#statusDot{width:.6rem;height:.6rem;border-radius:50%;background:#6a6f79;display:inline-block}
#statusText{color:var(--muted);font-size:.9rem}
#output{margin:0;padding:.6rem .8rem;overflow:auto;line-height:1.35;color:#a6ffad;background:#0c0f13;border-radius:0 0 10px 10px;white-space:pre-wrap;word-break:break-word;font-size:.93rem}
@media (max-width:960px){#blocklyDiv{height:56vh}#console{height:32vh}}
.blocklyMainBackground{fill:#0f1319 !important}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbblocks</h2>
<div class="spacer"></div>
<button class="btn" id="btnDemo">Demo laden</button>
<button class="btn ghost" id="btnZoomOut"></button>
<button class="btn ghost" id="btnZoomIn">+</button>
<button class="btn danger" id="btnTrash">Mülleimer</button>
<button class="btn primary" id="btnRun">Ausführen → Terminal</button>
</header>
<div id="wrap">
<div id="blocklyDiv" aria-label="Blockly Arbeitsfläche"></div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Logik" colour="#66b1ff">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_boolean"></block>
</category>
<category name="Schleifen" colour="#6cdf6c">
<block type="controls_repeat_ext">
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
</block>
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe" colour="#b68cff">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Text" colour="#ffd866">
<block type="text"></block>
<block type="text_print"></block>
</category>
<sep gap="8"></sep>
<category name="Variablen" custom="VARIABLE" colour="#ff75b5"></category>
<category name="Funktionen" custom="PROCEDURE" colour="#e056fd"></category>
</xml>
<section id="console" aria-label="Ausgabe">
<div id="consoleHead">
<span id="statusDot" title="Status"></span>
<strong>Konsole</strong>
<span id="statusText">bereit</span>
<div class="spacer"></div>
<button class="btn ghost" id="btnClear">Leeren</button>
<button class="btn ghost" id="btnToggle">Ein-/Ausklappen</button>
</div>
<pre id="output">🌲 Terminal wartet …</pre>
</section>
</div>
<script>
// eigenes Dark-Theme
const CrumbDark = Blockly.Theme.defineTheme('CrumbDark', {
base: Blockly.Themes.Dark,
componentStyles: {
workspaceBackgroundColour: '#0f1319',
toolboxBackgroundColour: '#0e1217',
toolboxForegroundColour: '#e9edf5',
flyoutBackgroundColour: '#0f1319',
flyoutForegroundColour: '#e9edf5',
scrollbarColour: '#2a3240',
insertionMarkerColour: '#4caf50',
insertionMarkerOpacity: 0.35,
cursorColour: '#4caf50',
}
});
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: CrumbDark,
renderer: 'zelos',
trashcan: true,
grid: { spacing: 28, length: 2, colour: '#1a2029', snap: true },
move: { scrollbars: true, drag: true, wheel: true },
zoom: { controls: false, wheel: true, startScale: 1.15, maxScale: 2.2, minScale: 0.7, scaleSpeed: 1.2 },
});
window.addEventListener('resize', () => Blockly.svgResize(workspace));
// VORDEFINIERTE VARIABLEN für die Kids
const PRESET_VARS = [
'energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s'
];
for (const name of PRESET_VARS) {
try { workspace.createVariable(name); } catch(_) {}
}
// UI helpers
const outputEl = document.getElementById('output');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const consoleBox = document.getElementById('console');
function setStatus(color, text){ statusDot.style.background=color; statusText.textContent=text||''; }
function log(line){ outputEl.textContent += (outputEl.textContent.endsWith('\n')?'':'\n')+ line; outputEl.scrollTop = outputEl.scrollHeight; }
function resetOutput(msg='🌲 Terminal wartet …'){ outputEl.textContent = msg; }
// Buttons
document.getElementById('btnZoomIn').onclick = () => workspace.zoomCenter(1);
document.getElementById('btnZoomOut').onclick = () => workspace.zoomCenter(-1);
document.getElementById('btnTrash').onclick = () => workspace.clear();
document.getElementById('btnClear').onclick = () => resetOutput('— leere Ausgabe —');
let collapsed=false;
document.getElementById('btnToggle').onclick = () => { collapsed=!collapsed; consoleBox.style.display = collapsed?'none':'grid'; };
// Demo-Workspace (Wasserkocher) via DOMParser, ohne Blockly.Xml.textToDom
const DEMO_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
</variables>
<block type="variables_set" x="20" y="20">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">800</field></block></value>
<next>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">10</field></block></value>
</block>
</value>
<next>
<block type="text_print">
<value name="TEXT">
<block type="text"><field name="TEXT">Erhitze… Temp=</field></block>
</value>
<next>
<block type="text_print">
<value name="TEXT"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Wasser kocht! 🔥</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>`.trim();
function loadDemo(){
try{
workspace.clear();
const dom = new DOMParser().parseFromString(DEMO_XML, 'text/xml').documentElement;
// domToWorkspace ist in allen Builds vorhanden
Blockly.Xml.domToWorkspace(dom, workspace);
Blockly.svgResize(workspace);
setStatus('#4caf50','Demo geladen');
resetOutput('🧪 Demo geladen „Wasserkocher“ bereit.');
}catch(e){
setStatus('#e74c3c','Demo-Fehler');
resetOutput('❌ Demo konnte nicht geladen werden:\n' + (e?.message||e));
console.error(e);
}
}
document.getElementById('btnDemo').onclick = loadDemo;
// Run nur aktiv wenn es Blöcke gibt
const btnRun = document.getElementById('btnRun');
function updateRunDisabled(){
const disabled = workspace.getAllBlocks(false).length===0;
btnRun.disabled = disabled;
btnRun.style.opacity = disabled ? .55 : 1;
btnRun.style.cursor = disabled ? 'not-allowed' : 'pointer';
}
workspace.addChangeListener(updateRunDisabled);
updateRunDisabled();
// Ausführen → Bridge (mit Echo-Fallback)
async function runCode(){
const code = Blockly.Python.workspaceToCode(workspace);
resetOutput('📤 Sende an Terminal…\n\n' + code);
setStatus('#f1c40f','sende…');
const ctrl = new AbortController();
const timer = setTimeout(()=>ctrl.abort(), 10000);
async function post(url){
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ blockcode: code }),
signal: ctrl.signal
});
clearTimeout(timer);
if(!res.ok) throw new Error('HTTP '+res.status+' '+(await res.text()).slice(0,400));
return res.json().catch(()=>({}));
}
try{
let data;
try{
// deine echte Bridge
data = await post('/crumbapi/blockly-terminal');
}catch(_){
// Fallback-Echo (damit Kids sofort Feedback sehen)
data = { ok:true, mode:'echo-fallback', code:code };
}
setStatus('#4caf50','fertig');
log('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
}catch(err){
setStatus('#e74c3c','Fehler');
log('\n❌ Fehler beim Senden:\n' + (err?.message||err));
}
}
btnRun.addEventListener('click', runCode);
// Startzustand: Demo direkt laden
loadDemo();
</script>
</body>
</html>

View File

@@ -0,0 +1,575 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Reject bei Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer, ~3,2/Z) • 22,2 V (Nominal) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20, // Full
v_storage: 3.80, // Storage
v_hv: 4.35, // HV
v_min: 3.20, // "ehrlich leer"
drift_reject: 0.30, // harte Zellabweichung ablehnen
tick_s: 10,
dv_charge: 0.01, // V/Z pro Tick (vereinfacht)
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Vordefinierte Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output Helpers
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){
const el = $('#output');
el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t;
el.scrollTop = el.scrollHeight;
}
window.appendOutput = appendOutput;
// === Patch 1: text_print -> Panel & Alerts abfangen ===
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// XML util
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// === DOC XML ===
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z → 25,2V • Storage 3,80V/Z → 22,8V • HV 4,35V/Z → 26,1V • Ende ~3,20V/Z → 19,2V.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance). Packs mit harten Unterschieden nicht nutzen.</field></block></value></block>
</xml>
`.trim();
// === T1: Storage (3.80V/Z) Entladen bis Ziel, mit Clamp ===
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value>
<next><block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</next></next></next></next>
</block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field>
<value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value>
</block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block>
</statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800">
<value name="TEXT">
<block type="text_join">
<mutation items="8"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* siehe 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T2: Full (4.20V/Z) Laden bis Ziel, mit Clamp ===
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value>
<next><block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</next></next></next></next>
</block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field>
<value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value>
</block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block>
</statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790">
<value name="TEXT">
<block type="text_join">
<mutation items="8"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* siehe 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T3: Reject bei Drift >= 0.30V ===
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270">
<value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350">
<value name="TEXT">
<block type="text_join">
<mutation items="10"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode": "safety_check", "max_v": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">max_v</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "min_v": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">min_v</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "delta": </field></block></value>
<value name="ADD5"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "stop_reason": "</field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">", "advice": "Pack nicht verwenden."</field></block></value>
<value name="ADD9"><block type="text"><field name="TEXT"> }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T4: HV 4.35V/Z Laden bis Ziel, mit Clamp ===
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820">
<value name="TEXT">
<block type="text_join">
<mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Zufalls-Startwerte (nur Inspektion) ===
function randomDemoXml(){
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>
`.trim();
}
// === Buttons ===
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Reject bei Drift ≥ 0,30V. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{ loadXml(randomDemoXml()); setOutput('🎲 Zufallspack geladen.'); };
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === Patch 2: Watchdog im Run-Handler ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
code = guardHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit (sichtbar für Kids/Trainer) -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (<i>≥0,30 V</i>) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,453 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v3)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v3)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>
`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820"><value name="TEXT"><block type="text_join"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
function randomDemoXml(){
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>
`.trim();
}
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{ loadXml(randomDemoXml()); setOutput('🎲 Zufallspack geladen.'); };
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
code = guardHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v4)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v4)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos (kompakt gehalten) ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = /* wie T1, nur 4.2V + dv=+0.01 + MIN/Clamp im If */;
const T3_XML = /* Drift-Check */;
const T4_XML = /* HV 4.35V */;
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// 1) Watchdog in jede while(...)
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// 2) Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// 3) Erzwinge Schreibvorgang + Clamp nach "new_v = cells[...] + dv;"
// - Variante mit 1-basiertem Index: cells[(i - 1)]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
// - Variante mit 0-basiertem Index: cells[i]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v4)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v4)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos (kompakt gehalten) ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = /* wie T1, nur 4.2V + dv=+0.01 + MIN/Clamp im If */;
const T3_XML = /* Drift-Check */;
const T4_XML = /* HV 4.35V */;
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// 1) Watchdog in jede while(...)
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// 2) Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// 3) Erzwinge Schreibvorgang + Clamp nach "new_v = cells[...] + dv;"
// - Variante mit 1-basiertem Index: cells[(i - 1)]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
// - Variante mit 0-basiertem Index: cells[i]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (DIAG v6)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root{--bg:#0f1115;--panel:#11151a;--txt:#e6edf3;--accent:#4caf50;--muted:#9aa4b2;--line:#21262d;--bad:#ff4d4f}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%;border:1px solid var(--line);border-radius:10px}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto;border:1px solid var(--line)}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto;border:1px solid var(--line)}
.hint{color:var(--muted);font-size:.9rem}
.fail{color:var(--bad);font-weight:700}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (DIAG v6)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear"class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg">Diag lädt …</div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- TOOLBOX (wird erst genutzt, wenn Blockly geladen ist) -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// ---------- Diagnose: Fehler ins Panel ----------
const out = document.getElementById('output');
const cfgEl = document.getElementById('cfg');
function setOutput(t){ out.textContent = t }
function appendOutput(t){ out.textContent += (out.textContent.endsWith('\n')?'':'\n') + t; out.scrollTop = out.scrollHeight; }
window.appendOutput = appendOutput;
window.addEventListener('error', (e)=> appendOutput('❌ JS-Error: ' + e.message));
window.addEventListener('unhandledrejection', (e)=> appendOutput('❌ Promise-Rejection: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)));
// ---------- Loader: erst lokal, dann CDN ----------
const CDN = 'https://unpkg.com';
const LOAD = [
['/crumbblocks/vendor/blockly/blockly.min.js', `${CDN}/blockly/blockly.min.js`],
['/crumbblocks/vendor/blockly/javascript.min.js', `${CDN}/blockly/javascript.min.js`],
['/crumbblocks/vendor/blockly/msg/de.js', `${CDN}/blockly/msg/de.js`],
];
function loadScriptPair([localSrc, cdnSrc]){
return new Promise((resolve, reject)=>{
const s = document.createElement('script');
s.src = localSrc;
s.onload = ()=> resolve({src:localSrc, ok:true, via:'local'});
s.onerror = ()=>{
const c = document.createElement('script');
c.src = cdnSrc;
c.onload = ()=> resolve({src:cdnSrc, ok:true, via:'cdn'});
c.onerror = ()=> reject(new Error('Laden fehlgeschlagen: ' + localSrc + ' und ' + cdnSrc));
document.head.appendChild(c);
};
document.head.appendChild(s);
});
}
// ---------- Self-Test ----------
async function selfTest(){
const diag = { js_ok:true, blockly_loaded:false, eval_ok:null, scripts:[], csp_note:null };
// Lade Reihenfolge wahren
for (const p of LOAD){
try {
const res = await loadScriptPair(p);
diag.scripts.push(res);
} catch (e) {
diag.scripts.push({src:p, ok:false, via:'none', err:String(e)});
}
}
diag.blockly_loaded = !!window.Blockly;
try { new Function('return 42')(); diag.eval_ok = true; }
catch(e){ diag.eval_ok = 'blocked: ' + e.message; diag.csp_note = 'CSP erlaubt kein unsafe-eval. Siehe Hinweise unten.'; }
const CFG = {
cells: 6, v_full:4.20, v_storage:3.80, v_hv:4.35, v_min:3.20,
drift_reject:0.30, tick_s:10, dv_charge:0.01, dv_discharge:-0.01,
capacity_mAh:2200, c_rate:1.0
};
cfgEl.textContent = 'Diag:\n' + JSON.stringify(diag, null, 2) + '\n\nCFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
if (!diag.blockly_loaded){
appendOutput('❌ Blockly nicht geladen. Prüfe /crumbblocks/vendor/... oder Internet/CSP.');
appendOutput('Tipp: Siehe „Lokale Vendor-Dateien“ unten.');
return;
}
bootBlockly(); // erst jetzt initialisieren
}
// ---------- Blockly-Init (nach bestandenem Self-Test) ----------
function bootBlockly(){
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// print ins Panel + Alert abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// 1-basige Listen sauber generieren
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// Helpers
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// Demos (kompakt)
const DOC_XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `<!-- Storage -->
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = T1_XML
.replace('storage','charge')
.replace('3.8','4.2')
.replace('"-0.01"','"0.01"')
.replace('storage_reached','full_reached')
.replace('"GT"','"LT"');
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>`.trim();
const T4_XML = T2_XML
.replace('charge','hv_charge')
.replace('4.2','4.35')
.replace('full_reached','hv_reached')
.replace(/3\.7[0-9]?/g,'3.96'); // Start näher an HV
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z. ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
}
// Start
selfTest();
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
<!-- Hinweise / Fallback -->
<div style="padding:.6rem .8rem;color:var(--muted)">
<b>Lokale Vendor-Dateien (empfohlen, falls CDN/CSP blockt):</b><br/>
<code>mkdir -p /var/www/crumbblocks/vendor/blockly</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/blockly.min.js https://unpkg.com/blockly/blockly.min.js</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/javascript.min.js https://unpkg.com/blockly/javascript.min.js</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/msg/de.js https://unpkg.com/blockly/msg/de.js</code><br/><br/>
<b>CSP (falls Eval/Inline blockiert → „nix passiert“):</b><br/>
Erweitere Header minimal für die Simulation:
<pre>Content-Security-Policy: default-src 'self';
script-src 'self' https://unpkg.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self';
img-src 'self' data:;
font-src 'self' data:;</pre>
(Nur für diese Lernseite. Produktion später strenger machen.)
</div>
</body>
</html>

View File

@@ -0,0 +1,500 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (DIAG v7)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root{--bg:#0f1115;--panel:#11151a;--txt:#e6edf3;--accent:#4caf50;--muted:#9aa4b2;--line:#21262d;--bad:#ff4d4f}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%;border:1px solid var(--line);border-radius:10px}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto;border:1px solid var(--line)}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto;border:1px solid var(--line)}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (DIAG v7)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear"class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg">Diag lädt …</div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z). Nur Simulation.
</div>
</div>
</div>
<!-- TOOLBOX -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// ---------- Diagnose & Output ----------
const out = document.getElementById('output');
const cfgEl = document.getElementById('cfg');
function setOutput(t){ out.textContent = t }
function appendOutput(t){ out.textContent += (out.textContent.endsWith('\n')?'':'\n') + t; out.scrollTop = out.scrollHeight; }
window.appendOutput = appendOutput;
window.addEventListener('error', e => appendOutput('❌ JS-Error: ' + e.message));
window.addEventListener('unhandledrejection', e => appendOutput('❌ Promise: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)));
// ---------- Loader: erst lokal, dann CDN (klassische, nicht-ESM Bundles) ----------
const CDN = 'https://unpkg.com';
const LOAD = [
['/crumbblocks/vendor/blockly/blockly_compressed.js', `${CDN}/blockly/blockly_compressed.js`],
['/crumbblocks/vendor/blockly/blocks_compressed.js', `${CDN}/blockly/blocks_compressed.js`],
['/crumbblocks/vendor/blockly/javascript_compressed.js', `${CDN}/blockly/javascript_compressed.js`],
['/crumbblocks/vendor/blockly/msg/de.js', `${CDN}/blockly/msg/de.js`],
];
function loadScriptPair([localSrc, cdnSrc]){
return new Promise((resolve, reject)=>{
const s = document.createElement('script');
s.src = localSrc;
s.onload = ()=> resolve({src:localSrc, ok:true, via:'local'});
s.onerror = ()=>{
const c = document.createElement('script');
c.src = cdnSrc;
c.onload = ()=> resolve({src:cdnSrc, ok:true, via:'cdn'});
c.onerror = ()=> reject(new Error('Laden fehlgeschlagen: ' + localSrc + ' und ' + cdnSrc));
document.head.appendChild(c);
};
document.head.appendChild(s);
});
}
async function selfTest(){
const diag = { blockly_loaded:false, generator_loaded:false, eval_ok:null, scripts:[], csp_note:null };
for (const p of LOAD){
try { diag.scripts.push(await loadScriptPair(p)); }
catch(e){ diag.scripts.push({src:p, ok:false, via:'none', err:String(e)}); }
}
diag.blockly_loaded = !!window.Blockly;
diag.generator_loaded = !!(window.Blockly && Blockly.JavaScript);
try { new Function('return 42')(); diag.eval_ok = true; }
catch(e){ diag.eval_ok = 'blocked: ' + e.message; diag.csp_note = 'CSP blockiert eval/new Function.'; }
const CFG = { cells:6, v_full:4.20, v_storage:3.80, v_hv:4.35, v_min:3.20, drift_reject:0.30, tick_s:10, dv_charge:0.01, dv_discharge:-0.01, capacity_mAh:2200, c_rate:1.0 };
cfgEl.textContent = 'Diag:\n' + JSON.stringify(diag, null, 2) + '\n\nCFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
if (!diag.blockly_loaded || !diag.generator_loaded){ appendOutput('❌ Blockly/Gernerator fehlt. Prüfe Loader/CDN/CSP.'); return; }
bootBlockly();
}
// ---------- Blockly-Init ----------
function bootBlockly(){
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Lern-Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// print → Panel
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert: ' + msg); } catch(_) { origAlert(msg); } };
})();
// 1-basiert Listen → richtig lesen/schreiben
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>
`.trim();
// T1: Storage 3.8V/Z
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T2: Full 4.2V/Z (dv +, MIN<target)
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T3: Drift-Check
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T4: HV 4.35V/Z (dv +, MIN<target)
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820"><value name="TEXT"><block type="text_join"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z. ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel + Writeback & Clamp
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// Watchdog in jede while
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// Writeback & Clamp
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
}
// Start
selfTest();
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Nur Simulation. Echte LiPos ausschließlich mit geeignetem Ladegerät & Balancer laden. Packs mit Zell-Delta ≥ 0,30 V nicht verwenden.
</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
// Minimal WebSocket echo server for testing Painter stream
// Usage: npm i ws; node mock_ws_server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 9980 });
console.log('WS server on ws://localhost:9980');
wss.on('connection', (ws)=>{
console.log('client connected');
ws.on('message', (msg)=>{
try{
const obj = JSON.parse(msg);
// log only essentials to keep console tidy
if(obj.type==='point'){
process.stdout.write(`• point ${obj.p.x.toFixed(1)},${obj.p.y.toFixed(1)}\r`);
}else{
console.log(obj.type);
}
}catch(e){}
// optionally echo back
// ws.send(msg);
});
ws.on('close', ()=>console.log('client closed'));
});

395
crumbblocks/painter.html Normal file
View File

@@ -0,0 +1,395 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter (MultiStroke) draw → export SVG + Motion JSON</title>
<style>
:root{
--bg: #0b0b10; --panel: #12121a; --muted:#8d93a1; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:340px 1fr;gap:16px;padding:16px}
@media (max-width:900px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0f131a;font-weight:600}
.btn.warn{background:var(--danger);color:#0f0f10}
.muted{color:var(--muted);font-size:.85rem}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:220px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter (MultiStroke) → SVG & Motion JSON • a→Spirale→y • Start/Stop/Dynamik</h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken setzen (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits 1+1 setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/StopMarker</span></label>
</div>
<div class="row">
<label>Strichstärke
<input type="range" id="stroke" min="4" max="28" step="1" value="12">
</label>
<label>Glättung
<input type="range" id="smooth" min="0" max="10" step="1" value="2">
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>StrokeFarbe A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>StrokeFarbe B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="row">
<label class="chip"><input type="checkbox" id="dynWidth"> <span>Breite ~ Geschwindigkeit (JSONonly)</span></label>
<label class="chip"><input type="checkbox" id="captureJSON" checked> <span>Motion JSON mitschreiben</span></label>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="StrichListe"></ul>
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/TouchDown</span>. Ende bei Loslassen. Mehrere Klicks/Wege werden einzeln erfasst.</p>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
<details>
<summary class="muted">A11y/Meta</summary>
<label>Titel <input id="title" type="text" value="Omi Omega OneLiner"></label>
<label>Beschreibung
<textarea id="desc" rows="3">Cursives kleines a wird zur Spirale; zwei Einsen im a; y hält zusammen; Funken ringsum.</textarea>
</label>
<label>Tag (z.B. Datum/Hashtag) <input id="tag" type="text" value="22.08.25 #CRUMB"></label>
</details>
<p class="muted">Tipps: Ziehen = zeichnen. <span class="kbd">Shift</span> = Funken. <span class="kbd">Z</span> = Undo. <span class="kbd">S</span> speichern.</p>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">Mehrere Pfade mit Start/Stop und Zeitdynamik: a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
</style>
</defs>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<text id="tagText" x="860" y="760" font-family="ui-monospace,monospace" font-size="22" fill="#cfd5e3"></text>
</g>
</svg>
</main>
</div>
<footer>
<span class="muted">Mehrere Klicks & Pfade sind erlaubt. Start/Stop wird als Ereignis erfasst; Geschwindigkeiten landen im MotionJSON.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const grad = document.getElementById('gradA2B');
const tagText = document.getElementById('tagText');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const strokeInp = document.getElementById('stroke');
const smoothInp = document.getElementById('smooth');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const dynWidth = document.getElementById('dynWidth');
const captureJSON = document.getElementById('captureJSON');
const titleInp = document.getElementById('title');
const descInp = document.getElementById('desc');
const tagInp = document.getElementById('tag');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null; // {id, mode, color, width, points:[{x,y,t}], pathEl}
const strokes = []; // array of strokes
let t0 = null; // session start time
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function updateMeta(){
stage.querySelector('title').textContent = titleInp.value.trim() || 'OneLiner';
stage.querySelector('desc').textContent = descInp.value.trim() || '';
tagText.textContent = tagInp.value.trim();
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px`;
li.append('Stroke', sel, sw, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function toPathD(points){
if(points.length===0) return '';
const sm = +smoothInp.value;
if(points.length<3 || sm===0){
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
// simple smoothing: use quadratic Beziers between midpoints
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i];
const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function startStroke(e){
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0 };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s.points)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function placeSpark(e){
const p = svgPoint(e);
const len = Math.max(14, parseInt(strokeInp.value,10)*1.5);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
function exportSVGString(){
updateMeta();
const clone = stage.cloneNode(true);
// inline live colors
clone.querySelector('#sparks')?.setAttribute('stroke', colorSInp.value);
// serialize
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>
${s}`;
}
function motionJSON(){
const meta = { title:titleInp.value, desc:descInp.value, tag:tagInp.value, t0: t0??0 };
const items = strokes.map(s=>{
// speeds
let maxV=0, sumV=0, nV=0; const pts=s.points;
for(let i=1;i<pts.length;i++){
const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; const dt=(pts[i].t-pts[i-1].t)/1000; if(dt<=0) continue;
const v = Math.hypot(dx,dy)/dt; maxV=Math.max(maxV,v); sumV+=v; nV++;
}
const avgV = nV? sumV/nV : 0;
return {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width,
startMs: s.start, durationMs: s.duration, lengthPx: s.length,
avgSpeedPxPerS: +avgV.toFixed(2), maxSpeedPxPerS: +maxV.toFixed(2),
points: s.points.map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}))
};
});
return JSON.stringify({meta, strokes:items}, null, 2);
}
// Buttons
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner_multi.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', endStroke);
// Hotkeys
window.addEventListener('keydown', (e)=>{
if(e.key==='z' || e.key==='Z'){ if(strokes.length){ removeStroke(strokes.length-1); } }
if((e.key==='s' || e.key==='S') && (e.ctrlKey||e.metaKey)){ e.preventDefault(); document.getElementById('downloadSVG').click(); }
});
// Reactive
[titleInp,descInp,tagInp].forEach(inp=>inp.addEventListener('input', updateMeta));
[colorAInp,colorBInp,colorSInp].forEach(inp=>inp.addEventListener('input', ()=>{ tagText.setAttribute('fill','#cfd5e3'); }));
updateMeta();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,647 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>One-Liner Painter + Bezier-Editor (Fix)</title>
<style>
:root{ --bg:#0b0b10; --panel:#12121a; --muted:#9aa3b2; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c; }
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none;font-size:.9rem;color:var(--muted)}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:370px 1fr;gap:16px;padding:16px}
@media (max-width:1000px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.col{display:grid;gap:10px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0b1114;font-weight:600}
.btn.warn{background:var(--danger);color:#0b0b10}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:240px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.badge{font-size:.75rem;color:#0b1114;background:var(--accent);border-radius:999px;padding:2px 8px}
.muted{color:var(--muted)}
/* Bezier UI visuals */
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
.anchor{fill:#31e07b;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handle{fill:#ffe08a;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handleLine{stroke:#ffe08a;stroke-width:2.5;stroke-dasharray:4 4; pointer-events:none}
.selected{filter:drop-shadow(0 0 4px #ffd166)}
#bezierUI{ pointer-events:none } /* UI-Schicht blockiert keine Klicks auf Pfad */
</style>
</head>
<body>
<header>
<h1>One-Liner Painter → SVG/PNG/JSON • Replay • <span class="badge">Bezier-Editor (Fix)</span></h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits „1“ setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/Stop-Marker</span></label>
<label class="chip"><input type="checkbox" id="frameOn" checked> <span>Crumblines-Frame</span></label>
</div>
<div class="row">
<label>Strichstärke <input type="range" id="stroke" min="4" max="28" step="1" value="12"></label>
<label>Sampler
<select id="sampler">
<option value="raw">Raw Polyline</option>
<option value="quad">Quadratic</option>
<option value="cubic" selected>Catmull-Rom → Cubic</option>
</select>
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>Stroke A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="col" style="margin-top:6px">
<strong>Bezier-Editor</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="editBezier"> <span>Bearbeiten</span></label>
<label class="chip"><input type="checkbox" id="linkHandles" checked> <span>Spiegel-Griffe</span></label>
<button class="btn" id="toBezier">Auswahl → Bezier</button>
<button class="btn" id="splitHandles">Anker: Split</button>
<button class="btn warn" id="delAnchor" disabled>Anker löschen</button>
</div>
<p class="muted">Klicke einen Stroke, dann <em>Bearbeiten</em>. Grüne Punkte = Anker, gelbe = Griffe. Ziehen. Doppelklick = Anker einfügen. <span class="kbd">Entf</span> löscht. <span class="kbd">L</span> spiegelt Griffe.</p>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="Strich-Liste"></ul>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="replay">▶︎ Replay</button>
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn" id="downloadPNG">PNG speichern</button>
<button class="btn accent" id="copyJSON">Motion-JSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega One-Liner</title>
<desc id="svgDesc">a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.selected{filter:drop-shadow(0 0 4px #ffd166)}
</style>
</defs>
<rect id="frameRect" x="16.5" y="16.5" width="1167" height="767" fill="none" stroke="rgba(255,255,255,.2)" stroke-width="1.5" stroke-dasharray="6 10"/>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<g id="bezierUI"></g>
</g>
</svg>
</main>
</div>
<footer>
<span>Fixes: keine (0,0)-Griffe mehr; Drag startet sofort; UI blockiert keine Klicks.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const bezierUI= document.getElementById('bezierUI');
const frameRect = document.getElementById('frameRect');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const frameOnCb = document.getElementById('frameOn');
const strokeInp = document.getElementById('stroke');
const samplerSel= document.getElementById('sampler');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const editBezierCb = document.getElementById('editBezier');
const linkHandlesCb= document.getElementById('linkHandles');
const toBezierBtn = document.getElementById('toBezier');
const splitBtn = document.getElementById('splitHandles');
const delAnchorBtn = document.getElementById('delAnchor');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const dlPNGBtn = document.getElementById('downloadPNG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
const replayBtn = document.getElementById('replay');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null;
const strokes = [];
let t0 = null;
let playing = false;
let selectedStroke = null;
let selectedAnchor = {stroke:null, idx:-1};
let dragging = null; // {type:'anchor'|'h1'|'h2', stroke, idx, ox, oy, pid}
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Bearbeiten';
edit.addEventListener('click',()=>{ selectStroke(s); editBezierCb.checked = true; drawBezierUI(); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px${s.isBezier?' • Bezier':''}`;
li.append('Stroke', sel, sw, edit, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
if(selectedStroke===s){ selectedStroke=null; clearBezierUI(); }
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
p.addEventListener('pointerdown', (e)=>{
if(editBezierCb.checked){ e.stopPropagation(); const s = strokes.find(st=>st.pathEl===p); if(s){ selectStroke(s); drawBezierUI(); } }
});
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
// Samplers → SVG path "d"
function pathRaw(points){
if(points.length===0) return '';
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
function pathQuadratic(points){
if(points.length<2) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i]; const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function pathCubic(points){
if(points.length<2) return '';
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const p=(i)=>points[ clamp(i,0,points.length-1) ];
let d=`M ${points[0].x} ${points[0].y}`;
for(let i=0;i<points.length-1;i++){
const p0=p(i-1), p1=p(i), p2=p(i+1), p3=p(i+2);
const c1x = p1.x + (p2.x - p0.x)/6, c1y = p1.y + (p2.y - p0.y)/6;
const c2x = p2.x - (p3.x - p1.x)/6, c2y = p2.y - (p3.y - p1.y)/6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
}
return d;
}
function toPathD(s){
if(s.isBezier) return pathFromAnchors(s.anchors);
const pts = s.points;
const smp = s.sampler || samplerSel.value;
if(smp==='raw') return pathRaw(pts);
if(smp==='cubic') return pathCubic(pts);
return pathQuadratic(pts);
}
function startStroke(e){
if(editBezierCb.checked){ return; } // während Edit nicht zeichnen
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0, sampler:samplerSel.value, isBezier:false };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function updatePath(s){
s.pathEl.setAttribute('d', toPathD(s));
s.pathEl.setAttribute('stroke-width', s.width);
}
function placeSpark(e){
const p = svgPoint(e);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
// --- Bezier conversion & editing ---
// FIX: build anchors with valid handles only; no (0,0) placeholders
function catmullToAnchors(points){
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const P=(i)=>points[ clamp(i,0,points.length-1) ];
const A=[];
for(let i=0;i<points.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1 = {x: p1.x + (p2.x - p0.x)/6, y: p1.y + (p2.y - p0.y)/6};
const c2 = {x: p2.x - (p3.x - p1.x)/6, y: p2.y - (p3.y - p1.y)/6};
if(i===0){
A.push({x:p1.x,y:p1.y,h1:{x:p1.x,y:p1.y},h2:{x:c1.x,y:c1.y},split:false});
}else{
// update previous anchor's outgoing handle
A[A.length-1].h2 = {x:c1.x,y:c1.y};
}
// next anchor for p2
const next = {x:p2.x,y:p2.y,h1:{x:c2.x,y:c2.y},h2:{x:p2.x,y:p2.y},split:false};
A.push(next);
}
return A;
}
function pathFromAnchors(A){
if(!A || A.length<2) return '';
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`;
}
return d;
}
function selectStroke(s){
selectedStroke = s;
strokes.forEach(st=> st.pathEl.classList.toggle('selected', st===s));
}
function clearBezierUI(){ bezierUI.innerHTML=''; selectedAnchor={stroke:null, idx:-1}; delAnchorBtn.disabled = true; }
function drawBezierUI(){
clearBezierUI();
const s = selectedStroke;
if(!editBezierCb.checked || !s || !s.isBezier) return;
s.anchors.forEach((a,idx)=>{
// handle lines
bezierUI.append(line(a.x, a.y, a.h1.x, a.h1.y, 'handleLine'));
bezierUI.append(line(a.x, a.y, a.h2.x, a.h2.y, 'handleLine'));
// handles
const h1 = circle(a.h1.x, a.h1.y, 5, 'handle', (e)=>startDrag('h1', s, idx, e));
const h2 = circle(a.h2.x, a.h2.y, 5, 'handle', (e)=>startDrag('h2', s, idx, e));
bezierUI.append(h1, h2);
// anchor
const an = circle(a.x, a.y, 6.5, 'anchor', (e)=>{ startDrag('anchor', s, idx, e) });
an.addEventListener('dblclick', (e)=>{ e.stopPropagation(); insertAnchorAt(s, idx); });
an.addEventListener('pointerdown', ()=>{ selectedAnchor={stroke:s,idx}; delAnchorBtn.disabled=false; });
bezierUI.append(an);
});
}
function line(x1,y1,x2,y2,cls){
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',x1); l.setAttribute('y1',y1);
l.setAttribute('x2',x2); l.setAttribute('y2',y2);
l.setAttribute('class',cls);
return l;
}
function circle(x,y,r,cls,onDown){
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class',cls);
c.style.touchAction = 'none';
c.addEventListener('pointerdown', (e)=>{ e.stopPropagation(); onDown(e); });
return c;
}
// FIX: start drag immediately on current event + proper pointer capture
function startDrag(type, s, idx, ev){
ev.preventDefault();
const a = s.anchors[idx];
const p0 = svgPoint(ev);
dragging = {type, stroke:s, idx, ox:p0.x, oy:p0.y, pid:ev.pointerId};
try { stage.setPointerCapture(ev.pointerId); } catch(e){}
const move = (e)=>{
if(!dragging) return;
const pos = svgPoint(e);
const dx = pos.x - dragging.ox, dy = pos.y - dragging.oy;
dragging.ox = pos.x; dragging.oy = pos.y;
if(type==='anchor'){
a.x+=dx; a.y+=dy; a.h1.x+=dx; a.h1.y+=dy; a.h2.x+=dx; a.h2.y+=dy;
}else if(type==='h1'){
a.h1.x+=dx; a.h1.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h2.x = a.x - (a.h1.x - a.x);
a.h2.y = a.y - (a.h1.y - a.y);
}
}else if(type==='h2'){
a.h2.x+=dx; a.h2.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h1.x = a.x - (a.h2.x - a.x);
a.h1.y = a.y - (a.h2.y - a.y);
}
}
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
};
const up = (e)=>{
try { stage.releasePointerCapture(ev.pointerId); } catch(_) {}
window.removeEventListener('pointermove', move);
dragging=null;
};
window.addEventListener('pointermove', move, {passive:true});
window.addEventListener('pointerup', up, {once:true});
}
function insertAnchorAt(s, idx){
const A = s.anchors;
if(idx>=A.length-1) return;
const a = A[idx], b = A[idx+1];
const mid = {x:(a.x+b.x)/2, y:(a.y+b.y)/2};
const dir = {x:(b.x-a.x), y:(b.y-a.y)};
const scale = 0.33;
const hLen = {x:dir.x*scale, y:dir.y*scale};
const newA = {
x: mid.x, y: mid.y,
h1: {x: mid.x - hLen.x*0.5, y: mid.y - hLen.y*0.5},
h2: {x: mid.x + hLen.x*0.5, y: mid.y + hLen.y*0.5},
split:false
};
A.splice(idx+1,0,newA);
s.pathEl.setAttribute('d', pathFromAnchors(A));
drawBezierUI();
}
function deleteSelectedAnchor(){
const {stroke:s, idx} = selectedAnchor;
if(!s || idx<0) return;
if(s.anchors.length<=2) return;
s.anchors.splice(idx,1);
selectedAnchor={stroke:null,idx:-1};
delAnchorBtn.disabled=true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
}
function convertToBezier(s){
if(s.isBezier) return;
if(s.points.length<2) return;
s.anchors = catmullToAnchors(s.points);
s.isBezier = true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
listRefresh();
}
toBezierBtn.addEventListener('click', ()=>{ if(selectedStroke) { convertToBezier(selectedStroke); drawBezierUI(); } });
splitBtn.addEventListener('click', ()=>{ if(selectedStroke && selectedAnchor.idx>=0){ const a=selectedStroke.anchors[selectedAnchor.idx]; a.split=!a.split; drawBezierUI(); }});
delAnchorBtn.addEventListener('click', deleteSelectedAnchor);
window.addEventListener('keydown', (e)=>{
if(e.key==='Delete' || e.key==='Backspace'){ if(editBezierCb.checked) { deleteSelectedAnchor(); } }
if(e.key==='l' || e.key==='L'){ linkHandlesCb.checked = !linkHandlesCb.checked; }
});
// Double-click on path to insert anchor near click
strokesG.addEventListener('dblclick', (e)=>{
if(!editBezierCb.checked || !selectedStroke || !selectedStroke.isBezier) return;
const p = svgPoint(e);
let bestI=0, bestD=1e9, A=selectedStroke.anchors;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
const d = Math.hypot(p.x-(a.x+b.x)/2, p.y-(a.y+b.y)/2);
if(d<bestD){ bestD=d; bestI=i; }
}
insertAnchorAt(selectedStroke, bestI);
});
// Export / Import / Replay (minimal for this fix)
function exportSVGString(){
frameRect.style.display = frameOnCb.checked ? 'block' : 'none';
const clone = stage.cloneNode(true);
// remove Bezier UI
const ui = clone.querySelector('#bezierUI'); if(ui) ui.remove();
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>\n${s}`;
}
function motionJSON(){
const items = strokes.map(s=>{
const o = {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width, sampler: s.sampler,
startMs: s.start||0, durationMs: s.duration||0, lengthPx: s.length||0,
};
if(s.isBezier){
o.isBezier = true;
o.anchors = s.anchors.map(a=>({x:a.x,y:a.y,h1:{x:a.h1.x,y:a.h1.y},h2:{x:a.h2.x,y:a.h2.y},split:!!a.split}));
}else{
o.points = (s.points||[]).map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}));
}
return o;
});
return JSON.stringify({strokes:items}, null, 2);
}
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; clearBezierUI(); listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
dlPNGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); svgToPng(svg, 'omi-omega-oneliner.png'); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('Motion-JSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
replayBtn.addEventListener('click', startReplay);
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', ()=>{ drawing=false; });
// init
frameRect.style.display = frameOnCb.checked ? 'block':'none';
// Replay
function startReplay(){
if(!strokes.length) return;
playing = true;
markersG.style.opacity = 0.25; bezierUI.style.opacity = 0.15;
strokes.forEach(s=> s.pathEl.setAttribute('d',''));
const total = strokes.reduce((m,s)=>Math.max(m, s.isBezier?1500:(s.points?.length ? s.points[s.points.length-1].t : 0)), 0);
const startWall = performance.now();
function step(){
if(!playing) return;
const t = performance.now() - startWall;
strokes.forEach(s=>{
if(s.isBezier){
const A=s.anchors;
const k = Math.min(A.length-1, Math.floor((t/total)*(A.length-1)) );
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<k;i++){ const a=A[i], b=A[i+1]; d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`; }
s.pathEl.setAttribute('d', d);
applyStrokeStyle(s);
s.pathEl.setAttribute('stroke-width', s.width);
}else{
const pts = s.points.filter(p => p.t <= t);
if(pts.length >= 2){
s.pathEl.setAttribute('stroke-width', s.width);
applyStrokeStyle(s);
const smp = s.sampler || samplerSel.value;
s.pathEl.setAttribute('d', smp==='raw'?pathRaw(pts): (smp==='cubic'?pathCubic(pts):pathQuadratic(pts)));
}
}
});
if(t < total) requestAnimationFrame(step);
else { playing=false; markersG.style.opacity = 1; bezierUI.style.opacity = 1; strokes.forEach(updatePath); }
}
requestAnimationFrame(step);
}
function svgToPng(svgString, filename){
const img = new Image();
const svgBlob = new Blob([svgString], {type:'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function(){
const canvas = document.createElement('canvas');
const vb = stage.viewBox.baseVal;
canvas.width = vb.width; canvas.height = vb.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob)=>{
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click();
});
URL.revokeObjectURL(url);
};
img.src = url;
}
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,395 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter (MultiStroke) draw → export SVG + Motion JSON</title>
<style>
:root{
--bg: #0b0b10; --panel: #12121a; --muted:#8d93a1; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:340px 1fr;gap:16px;padding:16px}
@media (max-width:900px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0f131a;font-weight:600}
.btn.warn{background:var(--danger);color:#0f0f10}
.muted{color:var(--muted);font-size:.85rem}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:220px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter (MultiStroke) → SVG & Motion JSON • a→Spirale→y • Start/Stop/Dynamik</h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken setzen (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits 1+1 setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/StopMarker</span></label>
</div>
<div class="row">
<label>Strichstärke
<input type="range" id="stroke" min="4" max="28" step="1" value="12">
</label>
<label>Glättung
<input type="range" id="smooth" min="0" max="10" step="1" value="2">
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>StrokeFarbe A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>StrokeFarbe B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="row">
<label class="chip"><input type="checkbox" id="dynWidth"> <span>Breite ~ Geschwindigkeit (JSONonly)</span></label>
<label class="chip"><input type="checkbox" id="captureJSON" checked> <span>Motion JSON mitschreiben</span></label>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="StrichListe"></ul>
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/TouchDown</span>. Ende bei Loslassen. Mehrere Klicks/Wege werden einzeln erfasst.</p>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
<details>
<summary class="muted">A11y/Meta</summary>
<label>Titel <input id="title" type="text" value="Omi Omega OneLiner"></label>
<label>Beschreibung
<textarea id="desc" rows="3">Cursives kleines a wird zur Spirale; zwei Einsen im a; y hält zusammen; Funken ringsum.</textarea>
</label>
<label>Tag (z.B. Datum/Hashtag) <input id="tag" type="text" value="22.08.25 #CRUMB"></label>
</details>
<p class="muted">Tipps: Ziehen = zeichnen. <span class="kbd">Shift</span> = Funken. <span class="kbd">Z</span> = Undo. <span class="kbd">S</span> speichern.</p>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">Mehrere Pfade mit Start/Stop und Zeitdynamik: a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
</style>
</defs>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<text id="tagText" x="860" y="760" font-family="ui-monospace,monospace" font-size="22" fill="#cfd5e3"></text>
</g>
</svg>
</main>
</div>
<footer>
<span class="muted">Mehrere Klicks & Pfade sind erlaubt. Start/Stop wird als Ereignis erfasst; Geschwindigkeiten landen im MotionJSON.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const grad = document.getElementById('gradA2B');
const tagText = document.getElementById('tagText');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const strokeInp = document.getElementById('stroke');
const smoothInp = document.getElementById('smooth');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const dynWidth = document.getElementById('dynWidth');
const captureJSON = document.getElementById('captureJSON');
const titleInp = document.getElementById('title');
const descInp = document.getElementById('desc');
const tagInp = document.getElementById('tag');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null; // {id, mode, color, width, points:[{x,y,t}], pathEl}
const strokes = []; // array of strokes
let t0 = null; // session start time
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function updateMeta(){
stage.querySelector('title').textContent = titleInp.value.trim() || 'OneLiner';
stage.querySelector('desc').textContent = descInp.value.trim() || '';
tagText.textContent = tagInp.value.trim();
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px`;
li.append('Stroke', sel, sw, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function toPathD(points){
if(points.length===0) return '';
const sm = +smoothInp.value;
if(points.length<3 || sm===0){
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
// simple smoothing: use quadratic Beziers between midpoints
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i];
const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function startStroke(e){
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0 };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s.points)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function placeSpark(e){
const p = svgPoint(e);
const len = Math.max(14, parseInt(strokeInp.value,10)*1.5);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
function exportSVGString(){
updateMeta();
const clone = stage.cloneNode(true);
// inline live colors
clone.querySelector('#sparks')?.setAttribute('stroke', colorSInp.value);
// serialize
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>
${s}`;
}
function motionJSON(){
const meta = { title:titleInp.value, desc:descInp.value, tag:tagInp.value, t0: t0??0 };
const items = strokes.map(s=>{
// speeds
let maxV=0, sumV=0, nV=0; const pts=s.points;
for(let i=1;i<pts.length;i++){
const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; const dt=(pts[i].t-pts[i-1].t)/1000; if(dt<=0) continue;
const v = Math.hypot(dx,dy)/dt; maxV=Math.max(maxV,v); sumV+=v; nV++;
}
const avgV = nV? sumV/nV : 0;
return {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width,
startMs: s.start, durationMs: s.duration, lengthPx: s.length,
avgSpeedPxPerS: +avgV.toFixed(2), maxSpeedPxPerS: +maxV.toFixed(2),
points: s.points.map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}))
};
});
return JSON.stringify({meta, strokes:items}, null, 2);
}
// Buttons
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner_multi.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', endStroke);
// Hotkeys
window.addEventListener('keydown', (e)=>{
if(e.key==='z' || e.key==='Z'){ if(strokes.length){ removeStroke(strokes.length-1); } }
if((e.key==='s' || e.key==='S') && (e.ctrlKey||e.metaKey)){ e.preventDefault(); document.getElementById('downloadSVG').click(); }
});
// Reactive
[titleInp,descInp,tagInp].forEach(inp=>inp.addEventListener('input', updateMeta));
[colorAInp,colorBInp,colorSInp].forEach(inp=>inp.addEventListener('input', ()=>{ tagText.setAttribute('fill','#cfd5e3'); }));
updateMeta();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,722 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🌈 Rainbow Counter v2 (ohne Anbindung)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly + JS Generator + Deutsche Labels -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root {
--bg: #1e1e1e;
--panel: #121212;
--txt: #e8e8e8;
--accent: #4caf50;
--muted: #a0a0a0;
--line: #2a2a2a
}
* {
box-sizing: border-box
}
html,
body {
height: 100%;
margin: 0;
background: var(--bg);
color: var(--txt);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace
}
header {
display: flex;
gap: .5rem;
align-items: center;
padding: .6rem .8rem;
border-bottom: 1px solid var(--line)
}
header h2 {
margin: 0;
font-size: 1rem;
font-weight: 700
}
header .sp {
flex: 1
}
button {
padding: .55rem .8rem;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 10px;
font-weight: 700;
cursor: pointer
}
#wrap {
display: flex;
gap: .8rem;
height: calc(100vh - 56px);
padding: .6rem .8rem
}
#left {
flex: 3;
display: flex;
flex-direction: column;
gap: .6rem
}
#right {
flex: 2;
display: flex;
flex-direction: column;
gap: .6rem
}
#blocklyDiv {
height: 60vh;
width: 100%
}
#output {
flex: 1;
min-height: 28vh;
background: var(--panel);
padding: 10px;
border-radius: 10px;
white-space: pre-wrap;
word-break: break-word;
overflow: auto
}
#cfg {
background: var(--panel);
padding: 10px;
border-radius: 10px;
white-space: pre-wrap;
overflow: auto
}
.hint {
color: var(--muted);
font-size: .9rem
}
.row {
display: flex;
gap: .4rem;
flex-wrap: wrap
}
.btn-secondary {
background: #3c99dc
}
.btn-warn {
background: #e67e22
}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • Rainbow Counter v2</h2>
<div class="sp"></div>
<div class="row">
<button id="btnDoc" title="Doku/Beispiel laden">📄 Doku</button>
<button id="btnT1" class="btn-secondary" title="Testfall 1: Basisliste">T1</button>
<button id="btnT2" class="btn-secondary" title="Testfall 2: Gleichstand">T2</button>
<button id="btnT3" class="btn-secondary" title="Testfall 3: Debounce=1">T3</button>
<button id="btnRnd" class="btn-warn" title="Zufalls-Demo laden">🎲 Zufall</button>
<button id="btnClear" title="Leeren">🗑️ Neu</button>
<button id="btnRun" title="Blockly-Code ausführen">▶️ Ausführen</button>
<button id="btnCrew" style="background:#9c27b0" title="Ergebnis an die Crew senden">🚀 An Crew senden</button>
</div>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“/„T1T3“ oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Ziel-JSON: {"total_events","classes","dominant","mapping"} •
Optional: Debounce (N), Gleichstand-Strategie (per Blocks).
</div>
</div>
</div>
<!-- Minimal-Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_forEach"></block>
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
<category name="Funktionen" custom="PROCEDURE"></category>
</xml>
<script>
// === CFG: Parameter-Kit (nur Anzeige/Didaktik, kein Netzwerk) ===
const CFG = {
classes: ["red", "green", "blue", "yellow"],
synonyms: { "purple": "unknown", "orange": "unknown" },
normalize: "case_insensitive",
window_n: 12,
debounce_n: 0,
dominant_mode: "max",
tie_policy: "all",
emit_mode: "final_only",
reset_mode: "manual",
include_ts: true,
mapping: { red: "⚠️", green: "✅", blue: "", yellow: "⏳", unknown: "❔" },
rng_seed: 42,
weights: { red: 1, green: 1, blue: 1, yellow: 1, unknown: 0.5 }
};
document.getElementById('cfg').textContent =
'CFG (Gedankenstütze):\\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: { spacing: 24, length: 3, colour: '#474747', snap: true },
trashcan: true,
zoom: { startScale: 1.1, maxScale: 2.0, minScale: .6, controls: false, wheel: true },
move: { scrollbars: true, drag: true, wheel: true }
});
// Vordefinierte Variablen (Zähler, Events, Dominant, optional last_ev)
['c_red', 'c_green', 'c_blue', 'c_yellow', 'c_unknown', 'total', 'ev', 'dominant', 'last_ev']
.forEach(v => { try { workspace.createVariable(v); } catch (_) { } });
// Output Helpers
const $ = s => document.querySelector(s);
function setOutput(t) { $('#output').textContent = t }
function appendOutput(t) {
const el = $('#output');
el.textContent += (el.textContent.endsWith('\\n') ? '' : '\\n') + t;
el.scrollTop = el.scrollHeight;
}
window.appendOutput = appendOutput; // für generierten Code
// text_print → Panel (statt alert)
// Wir suchen den Generator (je nach Version unterschiedlich)
const gen = (typeof Blockly.JavaScript !== 'undefined') ? Blockly.JavaScript :
(typeof javascript !== 'undefined' && javascript.javascriptGenerator) ? javascript.javascriptGenerator : null;
if (gen) {
gen['text_print'] = function (block) {
const argument0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || '\'\'';
return 'appendOutput(String(' + argument0 + '));\\n';
};
} else {
console.error("CRUMBBLOCKS: Konnte JavaScript Generator nicht finden!");
}
// XML Parser Fallback
function textToDom(xmlText) {
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch (_) { }
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml) { const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// === DOKU XML ===
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable>
</variables>
<!-- DOKU -->
<block type="text_print" x="20" y="20">
<value name="TEXT"><block type="text"><field name="TEXT">DOC: Regenbogen-Zähl-Maschine zählt Farbereignisse und erzeugt JSON-Report.</field></block></value>
<next><block type="text_print"><value name="TEXT"><block type="text"><field name="TEXT">Eingabe (Demo): ["red","green","blue","blue","yellow","purple"]</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="88">
<value name="TEXT"><block type="text"><field name="TEXT">Erwartet: {"total_events","classes":{"red","green","blue","yellow","unknown"},"dominant","mapping"}</field></block></value>
</block>
<!-- Init -->
<block type="variables_set" x="20" y="150">
<field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<!-- Demo-Ereignisse -->
<block type="controls_forEach" x="20" y="320">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">green</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD3"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">yellow</field></block></value>
<value name="ADD5"><block type="text"><field name="TEXT">purple</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">green</field></block></value>
</block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value>
</block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">yellow</field></block></value>
</block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
</block>
</next>
</block>
</statement>
</block>
<!-- Dominant (einfach) -->
<block type="variables_set" x="20" y="610">
<field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value>
</block>
<!-- JSON Report -->
<block type="text_print" x="20" y="650">
<value name="TEXT">
<block type="text_join">
<mutation items="22"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant": "</field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">", "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Testfälle ===
const T1_XML = DOC_XML; // Basisliste, dominant=blue (einfach)
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">T2: Gleichstand red vs blue</field></block></value></block>
<!-- Init -->
<block type="variables_set" x="20" y="70"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<!-- Events: ["red","blue","red","blue"] -->
<block type="controls_forEach" x="20" y="230">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD3"><block type="text"><field name="TEXT">blue</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="1" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="text_print"><value name="TEXT"><block type="text"><field name="TEXT">Ignoriere andere.</field></block></value></block></statement>
</block>
</next>
</block>
</statement>
</block>
<!-- Gleichstand: dominant = "red,blue" (Liste als Text) -->
<block type="variables_set" x="20" y="470"><field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">["red","blue"]</field></block></value>
</block>
<!-- JSON Report -->
<block type="text_print" x="20" y="520">
<value name="TEXT">
<block type="text_join">
<mutation items="24"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant_all": </field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">, "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable><variable>last_ev</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">T3: Debounce 1 (identische Wiederholung wird ignoriert)</field></block></value></block>
<!-- Init -->
<block type="variables_set" x="20" y="70"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<block type="variables_set" x="20" y="230"><field name="VAR">last_ev</field><value name="VALUE"><block type="text"><field name="TEXT">_none_</field></block></value></block>
<!-- Events: ["blue","blue","green"] -->
<block type="controls_forEach" x="20" y="280">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="3"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">green</field></block></value>
</block></value>
<statement name="DO">
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">NEQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">last_ev</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">green</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">yellow</field></block></value></block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
</block>
</next>
</block>
</statement>
</block>
<next><block type="variables_set"><field name="VAR">last_ev</field><value name="VALUE"><block type="variables_get"><field name="VAR">ev</field></block></value></block></next>
</statement>
</block>
<!-- Dominant (einfach max-Entscheid) -->
<block type="variables_set" x="20" y="620"><field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</block>
<block type="controls_if" x="20" y="660">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_red</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">red</field></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">green</field></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">yellow</field></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block></statement>
</block>
<!-- JSON -->
<block type="text_print" x="20" y="830">
<value name="TEXT">
<block type="text_join">
<mutation items="22"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant": "</field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">", "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Zufalls-Demo-Generator (nur Vorstellung, keine Sensorik) ===
function randomDemoXml(n = CFG.window_n) {
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange'];
// einfacher RNG (konstant)
let seed = CFG.rng_seed;
function rnd() { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }
const arr = Array.from({ length: n }, () => colors[Math.floor(rnd() * colors.length)]);
const items = arr.map((c, i) => `
<value name="ADD${i}"><block type="text"><field name="TEXT">${c}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>ev</variable><variable>total</variable><variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable><variable>c_yellow</variable><variable>c_unknown</variable><variable>dominant</variable></variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Zufalls-Ereignisse: ${arr.join(', ')}</field></block></value></block>
<block type="variables_set" x="20" y="80"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<block type="controls_forEach" x="20" y="260">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="${n}"></mutation>${items}</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if"><mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">green</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">yellow</field></block></value></block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
</block>
</next>
</statement>
</block>
<block type="variables_set" x="20" y="560"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block>
<block type="text_print" x="20" y="600"><value name="TEXT"><block type="text"><field name="TEXT">→ Ergänze IF-Kette oder eigene Funktion für „dominant“.</field></block></value></block>
</xml>
`.trim();
}
// === Actions ===
document.getElementById('btnDoc').onclick = () => { loadXml(DOC_XML); setOutput('📄 Doku geladen. Lies die DOC-Zeilen und drücke ▶️.'); };
document.getElementById('btnT1').onclick = () => { loadXml(T1_XML); setOutput('🧪 T1 geladen (Basisliste). ▶️ für Report.'); };
document.getElementById('btnT2').onclick = () => { loadXml(T2_XML); setOutput('🧪 T2 geladen (Gleichstand). ▶️ für Report mit dominant_all=["red","blue"].'); };
document.getElementById('btnT3').onclick = () => { loadXml(T3_XML); setOutput('🧪 T3 geladen (Debounce=1). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = () => { loadXml(randomDemoXml()); setOutput('🎲 Zufalls-Demo geladen. Ergänze Dominanz-Logik & ▶️.'); };
document.getElementById('btnClear').onclick = () => { workspace.clear(); setOutput('🧹 Workspace geleert.'); };
document.getElementById('btnRun').onclick = () => {
const gen = (typeof Blockly.JavaScript !== 'undefined') ? Blockly.JavaScript :
(typeof javascript !== 'undefined' && javascript.javascriptGenerator) ? javascript.javascriptGenerator : null;
if (!gen) { alert("Fehler: Generator nicht geladen."); return; }
const code = gen.workspaceToCode(workspace);
// Safety: Falls der Generator doch alert() nutzt, fangen wir das ab.
const oldAlert = window.alert;
window.alert = function (msg) { appendOutput(String(msg)); };
try {
new Function(code)();
appendOutput('\\n✅ Fertig.');
}
catch (e) { appendOutput('\\n❌ Fehler: ' + e); }
finally {
window.alert = oldAlert; // Restore
}
};
document.getElementById('btnCrew').onclick = async () => {
const outTxt = document.getElementById('output').textContent;
// Try to find the JSON part
const jsonMatch = outTxt.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
await navigator.clipboard.writeText(jsonMatch[0]);
alert('🚀 Daten kopiert!\n\nGehe jetzt in dein Terminal und führe aus:\n./missions/evaluate_mission_data.sh\n\n(Dort kannst du die Daten dann einfügen)');
} catch (err) {
alert('❌ Konnte nicht in die Zwischenablage kopieren: ' + err);
}
} else {
alert('⚠️ Kein gültiges JSON-Ergebnis gefunden!\nBitte führe erst den Code aus (▶️).');
}
};
</script>
</body>
</html>

31
crumbblocks/readme.txt Normal file
View File

@@ -0,0 +1,31 @@
# Schnippsi Painter — Streaming Quickstart
## 1) Datei
Öffne `schnippsi_painter_stream.html` im Browser. Zeichne frei oder mit dem BezierTool.
- `Verbinden` stellt eine WebSocket-Verbindung zu **ws://localhost:9980** her.
- Alle Events werden als JSON gesendet:
- `session` — Sessionstart
- `strokeStart` — neues Stroke (tool, colors, width)
- `point` — Punkte beim Freihandzeichnen (x,y,t,pressure,tilt)
- `anchor` / `anchors` — Bezier-Anker & Griffe (inkl. h1/h2)
- `strokeEnd` — Stroke abgeschlossen
## 2) TouchDesigner (ohne extra Code)
- Füge **WebSocket DAT** hinzu, setze **Mode = Server** und **Port = 9980**.
- Verbinde Browser (Button `Verbinden`).
- Hänge einen **JSON DAT** an den WebSocket-Ausgang (zum Parsen).
- Alternativ direkt WebSocket DAT -> **WebSocket In CHOP** (vereinfachte Skalare), für volle Struktur aber JSON DAT + Python DAT-Execute.
- Beispiel: Aus `point`-Messages `x,y` in einen **Trail CHOP** oder zum Zeichnen in einen **TOP**.
## 3) Test ohne TouchDesigner (Node)
Optionale MiniWS:
```bash
npm i ws
node mock_ws_server.js
```
Dann im Painter `ws://localhost:9980` verwenden.
## 4) Export
Buttons: JSON, SVG, PNG — jeweils Download.
A11y: Tastaturbedienung, ARIAStatus, hoher Kontrast, 200% Zoom stabil.

293
crumbblocks/schnippsi_ui.html Executable file
View File

@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dein Zeichen im Wald 🌿</title>
<style>
/* ✂️ SCHNIPPSI'S CSS LABOR ✂️ */
:root {
--bg-color: #121212;
--text-color: #e0e0e0;
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--accent: #4caf50;
/* Waldgrün */
--accent-hover: #66bb6a;
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-color);
color: var(--text-color);
/* Zentrierung: Das Herz des Layouts */
display: flex;
justify-content: center;
align-items: center;
/* Ein bisschen Wald-Atmosphäre */
background-image: radial-gradient(circle at 50% 50%, #1e2820 0%, #0a0e0b 100%);
}
/* Der Container für dein Zeichen */
.forest-container {
text-align: center;
}
/* Das "Zeichen" (Der Button/Trigger) */
.sign-box {
background: var(--glass-bg);
backdrop-filter: blur(10px);
/* Der Glassmorphism Effekt */
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 40px 60px;
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.3s ease, border-color 0.3s ease;
}
.sign-box:hover {
transform: translateY(-5px);
border-color: var(--accent);
}
.sign-icon {
font-size: 4rem;
margin-bottom: 10px;
display: block;
}
.sign-label {
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 1px;
}
/* 🌶️ DAS MODAL (Overlay) */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
/* Klick durchlassen wenn unsichtbar */
transition: opacity 0.3s ease;
z-index: 100;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal-content {
background: #1e1e1e;
border: 1px solid var(--accent);
border-radius: 15px;
padding: 30px;
width: 90%;
max-width: 400px;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.2);
transform: scale(0.9);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
h3 {
margin-top: 0;
color: var(--accent);
}
label {
display: block;
margin: 15px 0 5px;
font-size: 0.9rem;
color: #aaa;
text-align: left;
}
input,
textarea {
width: 100%;
padding: 10px;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 5px;
color: white;
font-family: inherit;
}
textarea {
height: 100px;
resize: none;
}
input:focus,
textarea:focus {
outline: none;
border-color: var(--accent);
}
.actions {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
.btn-cancel {
background: transparent;
color: #aaa;
border: 1px solid #444;
}
.btn-cancel:hover {
color: white;
border-color: white;
}
.btn-send {
background: var(--accent);
color: white;
}
.btn-submit:hover {
opacity: 0.9;
}
/* Nachricht nach dem Senden */
#status {
position: absolute;
bottom: 20px;
font-size: 0.9rem;
color: var(--accent);
opacity: 0;
transition: opacity 0.5s;
}
</style>
</head>
<body>
<!-- 🏛️ TEMPLATUS DOM STRUKTUR -->
<div class="forest-container">
<div class="sign-box" id="openModalBtn">
<span class="sign-icon">🌲</span>
<span class="sign-label">ZEICHEN SETZEN</span>
</div>
<div id="status">Daten kopiert! Zurück zum Terminal.</div>
</div>
<!-- MODAL -->
<div class="modal-overlay" id="modal">
<div class="modal-content">
<h3>Dein Zeichen im Wald</h3>
<p>Hinterlasse eine Nachricht für die anderen Krümel.</p>
<label for="inpName">Dein Name (oder Pseudonym)</label>
<input type="text" id="inpName" placeholder="z.B. Wanderer">
<label for="inpMsg">Deine Botschaft</label>
<textarea id="inpMsg" placeholder="Was hast du entdeckt?"></textarea>
<div class="actions">
<button class="btn-cancel" id="closeModalBtn">Abbrechen</button>
<button class="btn-send" id="sendBtn">🚀 Senden</button>
</div>
</div>
</div>
<script>
// 🌶️ PEPPER'S LOGIK LABOR
const modal = document.getElementById('modal');
const btnOpen = document.getElementById('openModalBtn');
const btnClose = document.getElementById('closeModalBtn');
const btnSend = document.getElementById('sendBtn');
const inpName = document.getElementById('inpName');
const inpMsg = document.getElementById('inpMsg');
const status = document.getElementById('status');
// Modal öffnen
btnOpen.addEventListener('click', () => {
modal.classList.add('active');
inpName.focus();
});
// Modal schließen
btnClose.addEventListener('click', () => {
modal.classList.remove('active');
});
// Klick auf Hintergrund schließt auch
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('active');
});
// Senden & JSON bauen
btnSend.addEventListener('click', async () => {
const name = inpName.value.trim() || 'Unbekannt';
const msg = inpMsg.value.trim() || 'Stilles Rauschen...';
// Das Datenpaket für die Crew
const data = {
mission: "schnippsi_ui",
author: name,
message: msg,
timestamp: new Date().toISOString(),
style: "glassmorphism"
};
const jsonStr = JSON.stringify(data, null, 2);
try {
await navigator.clipboard.writeText(jsonStr);
// UI Feedback
modal.classList.remove('active');
status.textContent = "✨ Copied! Füge es jetzt im Terminal ein: ./missions/evaluate_sign.sh";
status.style.opacity = '1';
// Nach 3s ausblenden
setTimeout(() => { status.style.opacity = '0'; }, 5000);
} catch (err) {
alert("Fehler beim Kopieren: " + err);
}
});
</script>
</body>
</html>