Here is a sheet of paper with a shape outlined on it. The whole point of the fold-and-cut theorem is that you can collapse the paper flat so that every edge of the outline lands on one single straight line — and then one cut along that line frees the shape.
<p class="hint">Pick a shape, then <b>Fold flat</b> to collapse every edge of its outline onto the single dashed cut line. <b>Cut</b> makes the one straight snip; <b>Unfold</b> drops the finished shape out. The whole trick lives in the folding — one cut always suffices (Demaine, Demaine & Lubiw, 1999).</p>
<div class="shapes">
<button class="pick on" data-shape="triangle" type="button">Triangle</button>
<button class="pick" data-shape="star" type="button">Star</button>
<button class="pick" data-shape="house" type="button">House</button>
</div>
<canvas id="paper" width="380" height="300"></canvas>
<div class="status" id="status">A shape is outlined on the sheet. Fold it flat.</div>
<div class="btns">
<button id="fold" type="button">Fold flat</button>
<button id="cut" type="button" disabled>Cut</button>
<button id="unfold" type="button" disabled>Unfold</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.shapes { display: flex; gap: .4rem; margin: 0 0 .5rem; }
.pick { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid #adb1b8;
background: #fff; color: #1d3557; border-radius: 7px; cursor: pointer; }
.pick.on { background: #1d3557; color: #fff; border-color: #1d3557; }
canvas { background: #fbf7ee; border: 1px solid #d8cdb5; border-radius: 8px; display: block; touch-action: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
// Fold-and-cut: any straight-edged shape can be folded flat so that EVERY edge of its outline lands on a single straight line; one cut along that line frees the shape (Demaine, Demaine & Lubiw, 1999). Here we collapse the outline onto a horizontal "cut line" by reflecting each vertex across that line via t in [0,1].
const cv = document.getElementById('paper'), ctx = cv.getContext('2d');
const W = cv.width, H = cv.height, CUT_Y = 150; // y of the single cut line
// Each shape: a closed polygon of vertices (already centered around the cut line region). Folding reflects the part below the line up onto the line: we animate every vertex's y toward CUT_Y, so all edges flatten onto the one line.
const SHAPES = {
triangle: [[110,60],[270,60],[190,240]],
star: (() => { const c=[190,150], R=95, r=38, p=[]; for(let i=0;i<10;i++){
const a=-Math.PI/2 + i*Math.PI/5, rad=i%2?r:R;
p.push([c[0]+rad*Math.cos(a), c[1]+rad*Math.sin(a)]); } return p; })(),
house: [[110,170],[110,90],[190,50],[270,90],[270,170],[230,170],[230,120],[150,120],[150,170]]
};
let shapeName = 'triangle', t = 0, phase = 'idle', cutDone = false, anim = null;
const lerp = (a,b,k) => a + (b-a)*k;
// A vertex (x,y) folded by fraction t: its y moves toward the cut line CUT_Y. At t=1 every vertex sits exactly on CUT_Y, so the whole outline is one segment.
function foldPt(p, k){ return [p[0], lerp(p[1], CUT_Y, k)]; }
function draw(){
ctx.clearRect(0,0,W,H);
// the cut line
ctx.save(); ctx.setLineDash([7,5]); ctx.strokeStyle = cutDone ? '#e63946' : '#b9a978';
ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(20,CUT_Y); ctx.lineTo(W-20,CUT_Y); ctx.stroke(); ctx.restore();
// label
ctx.fillStyle = '#9b8a63'; ctx.font = '12px system-ui';
ctx.fillText(cutDone ? 'cut' : 'cut line', W-70, CUT_Y-8);
const poly = SHAPES[shapeName];
// folded outline
ctx.beginPath();
poly.forEach((p,i)=>{ const f = foldPt(p, t); i ? ctx.lineTo(f[0],f[1]) : ctx.moveTo(f[0],f[1]); });
ctx.closePath();
ctx.lineWidth = 2.4; ctx.strokeStyle = '#1d3557';
ctx.fillStyle = phase==='unfolded' ? 'rgba(29,53,87,.12)' : 'rgba(185,169,120,.18)';
ctx.fill(); ctx.stroke();
// vertices
poly.forEach(p=>{ const f = foldPt(p,t); ctx.beginPath();
ctx.arc(f[0],f[1],3,0,7); ctx.fillStyle='#1d3557'; ctx.fill(); });
}
function setStatus(txt, ok){ const s=document.getElementById('status');
s.textContent = txt; s.className = 'status' + (ok?' ok':''); }
function btn(id){ return document.getElementById(id); }
function animateTo(target, after){
cancelAnimationFrame(anim);
const start = t, t0 = performance.now(), dur = 700;
(function step(now){ const k = Math.min(1,(now-t0)/dur);
t = lerp(start, target, k); draw();
if(k<1) anim = requestAnimationFrame(step); else if(after) after();
})(t0);
}
function fold(){ phase='folding'; cutDone=false;
btn('fold').disabled=true; btn('unfold').disabled=true;
animateTo(1, ()=>{ phase='flat'; btn('cut').disabled=false;
setStatus('Flat! Every edge of the outline now lies on the one cut line.', true); }); }
function cut(){ if(phase!=='flat') return; cutDone=true; phase='cut'; draw();
btn('cut').disabled=true; btn('unfold').disabled=false;
setStatus('One straight snip along the line — that single cut covers the whole outline.'); }
function unfold(){ if(!cutDone) return; phase='unfolded';
btn('unfold').disabled=true;
animateTo(0, ()=>{ setStatus('Unfolded — the exact shape falls out, from one cut. ✓', true); }); }
function reset(){ cancelAnimationFrame(anim); t=0; phase='idle'; cutDone=false;
btn('fold').disabled=false; btn('cut').disabled=true; btn('unfold').disabled=true;
setStatus('A shape is outlined on the sheet. Fold it flat.'); draw(); }
document.querySelectorAll('.pick').forEach(b=> b.addEventListener('click',()=>{
document.querySelectorAll('.pick').forEach(x=>x.classList.remove('on'));
b.classList.add('on'); shapeName = b.dataset.shape; reset(); }));
btn('fold').addEventListener('click', fold);
btn('cut').addEventListener('click', cut);
btn('unfold').addEventListener('click', unfold);
btn('reset').addEventListener('click', reset);
draw();
<p class="hint">Pick a shape, then <b>Fold flat</b> to collapse every edge of its outline onto the single dashed cut line. <b>Cut</b> makes the one straight snip; <b>Unfold</b> drops the finished shape out. The whole trick lives in the folding — one cut always suffices (Demaine, Demaine & Lubiw, 1999).</p>
<div class="shapes">
<button class="pick on" data-shape="triangle" type="button">Triangle</button>
<button class="pick" data-shape="star" type="button">Star</button>
<button class="pick" data-shape="house" type="button">House</button>
</div>
<canvas id="paper" width="380" height="300"></canvas>
<div class="status" id="status">A shape is outlined on the sheet. Fold it flat.</div>
<div class="btns">
<button id="fold" type="button">Fold flat</button>
<button id="cut" type="button" disabled>Cut</button>
<button id="unfold" type="button" disabled>Unfold</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.shapes { display: flex; gap: .4rem; margin: 0 0 .5rem; }
.pick { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid #adb1b8;
background: #fff; color: #1d3557; border-radius: 7px; cursor: pointer; }
.pick.on { background: #1d3557; color: #fff; border-color: #1d3557; }
canvas { background: #fbf7ee; border: 1px solid #d8cdb5; border-radius: 8px; display: block; touch-action: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
// Fold-and-cut: any straight-edged shape can be folded flat so that EVERY edge of its outline lands on a single straight line; one cut along that line frees the shape (Demaine, Demaine & Lubiw, 1999). Here we collapse the outline onto a horizontal "cut line" by reflecting each vertex across that line via t in [0,1].
const cv = document.getElementById('paper'), ctx = cv.getContext('2d');
const W = cv.width, H = cv.height, CUT_Y = 150; // y of the single cut line
// Each shape: a closed polygon of vertices (already centered around the cut line region). Folding reflects the part below the line up onto the line: we animate every vertex's y toward CUT_Y, so all edges flatten onto the one line.
const SHAPES = {
triangle: [[110,60],[270,60],[190,240]],
star: (() => { const c=[190,150], R=95, r=38, p=[]; for(let i=0;i<10;i++){
const a=-Math.PI/2 + i*Math.PI/5, rad=i%2?r:R;
p.push([c[0]+rad*Math.cos(a), c[1]+rad*Math.sin(a)]); } return p; })(),
house: [[110,170],[110,90],[190,50],[270,90],[270,170],[230,170],[230,120],[150,120],[150,170]]
};
let shapeName = 'triangle', t = 0, phase = 'idle', cutDone = false, anim = null;
const lerp = (a,b,k) => a + (b-a)*k;
// A vertex (x,y) folded by fraction t: its y moves toward the cut line CUT_Y. At t=1 every vertex sits exactly on CUT_Y, so the whole outline is one segment.
function foldPt(p, k){ return [p[0], lerp(p[1], CUT_Y, k)]; }
function draw(){
ctx.clearRect(0,0,W,H);
// the cut line
ctx.save(); ctx.setLineDash([7,5]); ctx.strokeStyle = cutDone ? '#e63946' : '#b9a978';
ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(20,CUT_Y); ctx.lineTo(W-20,CUT_Y); ctx.stroke(); ctx.restore();
// label
ctx.fillStyle = '#9b8a63'; ctx.font = '12px system-ui';
ctx.fillText(cutDone ? 'cut' : 'cut line', W-70, CUT_Y-8);
const poly = SHAPES[shapeName];
// folded outline
ctx.beginPath();
poly.forEach((p,i)=>{ const f = foldPt(p, t); i ? ctx.lineTo(f[0],f[1]) : ctx.moveTo(f[0],f[1]); });
ctx.closePath();
ctx.lineWidth = 2.4; ctx.strokeStyle = '#1d3557';
ctx.fillStyle = phase==='unfolded' ? 'rgba(29,53,87,.12)' : 'rgba(185,169,120,.18)';
ctx.fill(); ctx.stroke();
// vertices
poly.forEach(p=>{ const f = foldPt(p,t); ctx.beginPath();
ctx.arc(f[0],f[1],3,0,7); ctx.fillStyle='#1d3557'; ctx.fill(); });
}
function setStatus(txt, ok){ const s=document.getElementById('status');
s.textContent = txt; s.className = 'status' + (ok?' ok':''); }
function btn(id){ return document.getElementById(id); }
function animateTo(target, after){
cancelAnimationFrame(anim);
const start = t, t0 = performance.now(), dur = 700;
(function step(now){ const k = Math.min(1,(now-t0)/dur);
t = lerp(start, target, k); draw();
if(k<1) anim = requestAnimationFrame(step); else if(after) after();
})(t0);
}
function fold(){ phase='folding'; cutDone=false;
btn('fold').disabled=true; btn('unfold').disabled=true;
animateTo(1, ()=>{ phase='flat'; btn('cut').disabled=false;
setStatus('Flat! Every edge of the outline now lies on the one cut line.', true); }); }
function cut(){ if(phase!=='flat') return; cutDone=true; phase='cut'; draw();
btn('cut').disabled=true; btn('unfold').disabled=false;
setStatus('One straight snip along the line — that single cut covers the whole outline.'); }
function unfold(){ if(!cutDone) return; phase='unfolded';
btn('unfold').disabled=true;
animateTo(0, ()=>{ setStatus('Unfolded — the exact shape falls out, from one cut. ✓', true); }); }
function reset(){ cancelAnimationFrame(anim); t=0; phase='idle'; cutDone=false;
btn('fold').disabled=false; btn('cut').disabled=true; btn('unfold').disabled=true;
setStatus('A shape is outlined on the sheet. Fold it flat.'); draw(); }
document.querySelectorAll('.pick').forEach(b=> b.addEventListener('click',()=>{
document.querySelectorAll('.pick').forEach(x=>x.classList.remove('on'));
b.classList.add('on'); shapeName = b.dataset.shape; reset(); }));
btn('fold').addEventListener('click', fold);
btn('cut').addEventListener('click', cut);
btn('unfold').addEventListener('click', unfold);
btn('reset').addEventListener('click', reset);
draw();
Comments
Loading comments...