|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Angle Bisectors of a Triangle Meet at a Point — the Incenter</title> |
|
<style> |
|
:root { |
|
--bg: #0f172a; |
|
--card-bg: #1e293b; |
|
--accent-a: #38bdf8; |
|
--accent-b: #f43f5e; |
|
--bisector: #f59e0b; |
|
--perp: #22c55e; |
|
--side: #94a3b8; |
|
--text: #f8fafc; |
|
--text-muted: #94a3b8; |
|
--btn: #334155; |
|
--btn-hover: #475569; |
|
} |
|
* { box-sizing: border-box; margin: 0; padding: 0; |
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; } |
|
body { |
|
background-color: var(--bg); |
|
color: var(--text); |
|
min-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
padding: 2rem 1rem; |
|
} |
|
header { text-align: center; max-width: 860px; margin-bottom: 1.5rem; } |
|
h1 { font-size: 1.6rem; margin-bottom: 0.4rem; } |
|
p.subtitle { color: var(--text-muted); font-size: 0.95rem; line-height: 1.4; } |
|
.theorem { |
|
display: inline-block; margin-top: 0.6rem; padding: 0.45rem 1rem; |
|
border: 1px solid #33415577; border-left: 3px solid var(--bisector); |
|
border-radius: 6px; font-size: 0.95rem; color: var(--text); |
|
} |
|
.container { |
|
display: flex; flex-direction: column; gap: 1.5rem; |
|
width: 100%; max-width: 1080px; |
|
} |
|
@media (min-width: 860px) { |
|
.container { display: grid; grid-template-columns: 1fr 340px; } |
|
} |
|
.canvas-card, .side-card { |
|
background-color: var(--card-bg); |
|
border-radius: 12px; |
|
padding: 1rem; |
|
box-shadow: 0 10px 25px -5px rgba(0,0,0,0.3); |
|
} |
|
.canvas-card { display: flex; flex-direction: column; align-items: center; } |
|
svg { width: 100%; height: auto; max-height: 540px; background: var(--bg); |
|
border-radius: 8px; touch-action: none; } |
|
.drag-hint { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.5rem; } |
|
.side-card { display: flex; flex-direction: column; gap: 1rem; padding: 1.3rem; } |
|
.proof-title { color: var(--bisector); font-size: 0.8rem; font-weight: 700; |
|
letter-spacing: 0.08em; text-transform: uppercase; } |
|
ol.proof { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; } |
|
ol.proof li { |
|
font-size: 0.88rem; line-height: 1.45; color: var(--text-muted); |
|
border-left: 3px solid var(--btn); padding: 0.3rem 0 0.3rem 0.7rem; |
|
transition: color 0.2s, border-color 0.2s; |
|
} |
|
ol.proof li.active { color: var(--text); border-color: var(--bisector); } |
|
ol.proof li.done { color: #cbd5e1; } |
|
ol.proof .m { font-family: Cambria, 'STIX Two Math', 'Times New Roman', serif; |
|
font-style: italic; } |
|
.readout { |
|
background: #0f172a; border-radius: 10px; padding: 0.7rem 0.9rem; |
|
display: flex; justify-content: space-around; text-align: center; |
|
font-variant-numeric: tabular-nums; |
|
} |
|
.readout .val { font-size: 1.15rem; font-weight: 700; color: var(--perp); } |
|
.readout .cap { font-size: 0.72rem; color: var(--text-muted); } |
|
.btn-row { display: flex; gap: 0.5rem; flex-wrap: wrap; } |
|
button { |
|
background: var(--btn); color: var(--text); border: none; |
|
border-radius: 8px; padding: 0.55rem 0.9rem; font-size: 0.9rem; |
|
cursor: pointer; transition: background 0.15s; |
|
} |
|
button:hover:not(:disabled) { background: var(--btn-hover); } |
|
button:disabled { opacity: 0.55; cursor: default; } |
|
button.primary { background: #0369a1; } |
|
button.primary:hover:not(:disabled) { background: #0284c7; } |
|
button.gold { background: #92600a; } |
|
button.gold:hover:not(:disabled) { background: #b45309; } |
|
footer { margin-top: 1.5rem; color: var(--text-muted); font-size: 0.85rem; } |
|
footer a { color: var(--accent-a); text-decoration: none; margin: 0 0.3rem; } |
|
footer a:hover { text-decoration: underline; } |
|
</style> |
|
</head> |
|
<body> |
|
<header> |
|
<h1>The Angle Bisectors of a Triangle All Meet at a Point</h1> |
|
<p class="subtitle">An animated proof built on the last two pages: the equidistance |
|
theorem and its converse.</p> |
|
<div class="theorem"><b>Theorem.</b> In any <span>△ABC</span>, the bisectors of |
|
∠A, ∠B and ∠C are concurrent — they meet at the incenter D.</div> |
|
</header> |
|
|
|
<div class="container"> |
|
<div class="canvas-card"> |
|
<svg id="scene" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg"> |
|
<!-- triangle sides --> |
|
<line id="sideAB" stroke="#94a3b8" stroke-width="3" stroke-linecap="round"/> |
|
<line id="sideBC" stroke="#94a3b8" stroke-width="3" stroke-linecap="round"/> |
|
<line id="sideCA" stroke="#94a3b8" stroke-width="3" stroke-linecap="round"/> |
|
<!-- incircle --> |
|
<circle id="incircle" fill="#f59e0b" fill-opacity="0.07" stroke="#f59e0b" |
|
stroke-width="2" stroke-dasharray="6 6" opacity="0"/> |
|
<!-- bisector cevians --> |
|
<line id="cevA" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round"/> |
|
<line id="cevB" stroke="#f43f5e" stroke-width="2.5" stroke-linecap="round"/> |
|
<line id="cevC" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/> |
|
<!-- equal-angle marks (populated by script) --> |
|
<g id="marksA"></g> |
|
<g id="marksB"></g> |
|
<g id="marksC"></g> |
|
<!-- perpendiculars from D --> |
|
<line id="perpE" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round"/> |
|
<line id="perpF" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round"/> |
|
<line id="perpG" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round"/> |
|
<line id="tickE" stroke="#22c55e" stroke-width="2"/> |
|
<line id="tickF" stroke="#22c55e" stroke-width="2"/> |
|
<line id="tickG" stroke="#22c55e" stroke-width="2"/> |
|
<polyline id="sqE" fill="none" stroke="#22c55e" stroke-width="1.5" opacity="0.85"/> |
|
<polyline id="sqF" fill="none" stroke="#22c55e" stroke-width="1.5" opacity="0.85"/> |
|
<polyline id="sqG" fill="none" stroke="#22c55e" stroke-width="1.5" opacity="0.85"/> |
|
<!-- points --> |
|
<circle id="ptD" fill="#f59e0b" stroke="#0f172a" stroke-width="1.5"/> |
|
<circle id="ptE" r="0" fill="#22c55e"/> |
|
<circle id="ptF" r="0" fill="#22c55e"/> |
|
<circle id="ptG" r="0" fill="#22c55e"/> |
|
<circle id="vA" r="6" fill="#f8fafc"/> |
|
<circle id="vB" r="6" fill="#f8fafc"/> |
|
<circle id="vC" r="6" fill="#f8fafc"/> |
|
<!-- drag handles --> |
|
<circle id="hitA" r="18" fill="transparent" cursor="grab"/> |
|
<circle id="hitB" r="18" fill="transparent" cursor="grab"/> |
|
<circle id="hitC" r="18" fill="transparent" cursor="grab"/> |
|
<!-- labels --> |
|
<text id="lblA" font-size="18" fill="#f8fafc" font-weight="600" text-anchor="middle">A</text> |
|
<text id="lblB" font-size="18" fill="#f8fafc" font-weight="600" text-anchor="middle">B</text> |
|
<text id="lblC" font-size="18" fill="#f8fafc" font-weight="600" text-anchor="middle">C</text> |
|
<text id="lblD" font-size="17" fill="#f59e0b" font-weight="700">D</text> |
|
<text id="lblE" font-size="15" fill="#22c55e" text-anchor="middle">E</text> |
|
<text id="lblF" font-size="15" fill="#22c55e" text-anchor="middle">F</text> |
|
<text id="lblG" font-size="15" fill="#22c55e" text-anchor="middle">G</text> |
|
<text id="caption" font-size="18" fill="#f59e0b" font-weight="600" |
|
text-anchor="middle" x="400" y="486">∴ the three bisectors meet at D — the incenter of △ABC</text> |
|
</svg> |
|
<div class="drag-hint">Tip: drag the vertices A, B, C to reshape the triangle at any step.</div> |
|
</div> |
|
|
|
<div class="side-card"> |
|
<div class="proof-title" id="stepBadge">Proof — step 1 of 5</div> |
|
<ol class="proof" id="proofList"> |
|
<li><span class="m">1.</span> Draw △ABC.</li> |
|
<li><span class="m">2.</span> Draw the bisectors of ∠A and ∠B. |
|
Let them meet at a point D.</li> |
|
<li><span class="m">3.</span> Drop perpendiculars from D to the three sides: |
|
DE ⊥ AB, DF ⊥ BC, DG ⊥ CA. By the equidistance theorem |
|
(page 2), DE = DF and DE = DG, so |
|
DE = DF = DG.</li> |
|
<li><span class="m">4.</span> Join DC. Since DF = DG, D is |
|
equidistant from sides CB and CA ⇒ by the converse (page 3), |
|
DC bisects ∠C.</li> |
|
<li><span class="m">5.</span> ∴ all three angle bisectors pass through |
|
the single point D — the <b>incenter</b>, center of the inscribed circle |
|
with radius r = DE.</li> |
|
</ol> |
|
<div class="readout"> |
|
<div><div class="val" id="deVal">—</div><div class="cap">DE</div></div> |
|
<div><div class="val" id="dfVal">—</div><div class="cap">DF</div></div> |
|
<div><div class="val" id="dgVal">—</div><div class="cap">DG</div></div> |
|
</div> |
|
<div class="btn-row"> |
|
<button id="playBtn" class="primary">❚❚ Pause</button> |
|
<button id="resetBtn">↺ Reset</button> |
|
<button id="prevBtn">←</button> |
|
<button id="nextBtn">→</button> |
|
</div> |
|
<div class="btn-row"> |
|
<button id="gifBtn" class="gold">⬇ Download GIF</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<footer> |
|
Angle bisector series: |
|
<a href="1-construction.html">1·Construction</a> |
|
<a href="2-equidistance.html">2·Equidistance</a> |
|
<a href="3-converse.html">3·Converse</a> |
|
<a href="4-incenter.html">4·Incenter</a> |
|
</footer> |
|
|
|
<script src="gif-export.js"></script> |
|
<script> |
|
(function () { |
|
'use strict'; |
|
|
|
var $ = function (id) { return document.getElementById(id); }; |
|
var svg = $('scene'); |
|
var NS = 'http://www.w3.org/2000/svg'; |
|
|
|
var A = { x: 150, y: 425 }; |
|
var B = { x: 655, y: 425 }; |
|
var C = { x: 420, y: 80 }; |
|
|
|
var DUR = 11000, HOLD = 2000; |
|
var PH = [0, 0.13, 0.33, 0.60, 0.82, 1.00]; |
|
var T = 0, playing = true, recording = false, last = null; |
|
var dragging = null; |
|
|
|
/* ---------- geometry ---------- */ |
|
|
|
function dist(P, Q) { return Math.hypot(Q.x - P.x, Q.y - P.y); } |
|
function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } |
|
function ease(v) { return v * v * (3 - 2 * v); } |
|
function lerpPt(P, Q, t) { return { x: P.x + (Q.x - P.x) * t, y: P.y + (Q.y - P.y) * t }; } |
|
function ang(V, P) { return Math.atan2(-(P.y - V.y), P.x - V.x) * 180 / Math.PI; } |
|
|
|
function incenter() { |
|
var a = dist(B, C), b = dist(C, A), c = dist(A, B); |
|
var s = a + b + c; |
|
return { x: (a * A.x + b * B.x + c * C.x) / s, |
|
y: (a * A.y + b * B.y + c * C.y) / s }; |
|
} |
|
function project(P, U, V) { // foot of the perpendicular from P onto line UV |
|
var ux = V.x - U.x, uy = V.y - U.y; |
|
var t = ((P.x - U.x) * ux + (P.y - U.y) * uy) / (ux * ux + uy * uy); |
|
return { x: U.x + t * ux, y: U.y + t * uy }; |
|
} |
|
function lineHit(P, Q, U, V) { // intersection of line PQ with line UV |
|
var d1x = Q.x - P.x, d1y = Q.y - P.y, d2x = V.x - U.x, d2y = V.y - U.y; |
|
var den = d1x * d2y - d1y * d2x; |
|
var t = ((U.x - P.x) * d2y - (U.y - P.y) * d2x) / den; |
|
return { x: P.x + t * d1x, y: P.y + t * d1y }; |
|
} |
|
function arcPath(c, r, a0, a1) { // math angles in degrees |
|
if (Math.abs(a1 - a0) < 0.05) return ''; |
|
var p0 = { x: c.x + r * Math.cos(a0 * Math.PI / 180), y: c.y - r * Math.sin(a0 * Math.PI / 180) }; |
|
var p1 = { x: c.x + r * Math.cos(a1 * Math.PI / 180), y: c.y - r * Math.sin(a1 * Math.PI / 180) }; |
|
var large = Math.abs(a1 - a0) > 180 ? 1 : 0; |
|
var sweep = a1 > a0 ? 0 : 1; |
|
return 'M ' + p0.x.toFixed(2) + ' ' + p0.y.toFixed(2) + |
|
' A ' + r + ' ' + r + ' 0 ' + large + ' ' + sweep + ' ' + |
|
p1.x.toFixed(2) + ' ' + p1.y.toFixed(2); |
|
} |
|
// arc between two directions, taking the short way around |
|
function arcBetween(V, aFrom, aTo, r) { |
|
var d = ((aTo - aFrom + 540) % 360) - 180; |
|
return arcPath(V, r, aFrom, aFrom + d); |
|
} |
|
function setLine(el, a, b) { |
|
el.setAttribute('x1', a.x); el.setAttribute('y1', a.y); |
|
el.setAttribute('x2', b.x); el.setAttribute('y2', b.y); |
|
} |
|
function place(el, x, y) { el.setAttribute('x', x); el.setAttribute('y', y); } |
|
function centroid() { return { x: (A.x + B.x + C.x) / 3, y: (A.y + B.y + C.y) / 3 }; } |
|
function outward(V, d) { // point pushed d px away from the triangle's center |
|
var g = centroid(); |
|
var L = dist(V, g) || 1; |
|
return { x: V.x + (V.x - g.x) / L * d, y: V.y + (V.y - g.y) / L * d }; |
|
} |
|
function squarePts(F, u, v, size) { |
|
var a = { x: F.x - u.x * size, y: F.y - u.y * size }; |
|
var b = { x: a.x + v.x * size, y: a.y + v.y * size }; |
|
var c = { x: F.x + v.x * size, y: F.y + v.y * size }; |
|
return a.x.toFixed(1) + ',' + a.y.toFixed(1) + ' ' + |
|
b.x.toFixed(1) + ',' + b.y.toFixed(1) + ' ' + |
|
c.x.toFixed(1) + ',' + c.y.toFixed(1); |
|
} |
|
function perpTick(el, P, Q) { // small equality tick crossing segment PQ at its midpoint |
|
var mx = (P.x + Q.x) / 2, my = (P.y + Q.y) / 2; |
|
var L = dist(P, Q) || 1; |
|
var nx = -(Q.y - P.y) / L, ny = (Q.x - P.x) / L; |
|
setLine(el, { x: mx - 5 * nx, y: my - 5 * ny }, { x: mx + 5 * nx, y: my + 5 * ny }); |
|
} |
|
|
|
/* ---------- equal-angle marks: n concentric arcs per half-angle ---------- */ |
|
|
|
function makeArcs(groupId, n, color) { |
|
var g = $(groupId), arr = []; |
|
for (var k = 0; k < 2 * n; k++) { |
|
var p = document.createElementNS(NS, 'path'); |
|
p.setAttribute('fill', 'none'); |
|
p.setAttribute('stroke', color); |
|
p.setAttribute('stroke-width', '1.8'); |
|
g.appendChild(p); |
|
arr.push(p); |
|
} |
|
return arr; |
|
} |
|
var arcsA = makeArcs('marksA', 1, '#38bdf8'); |
|
var arcsB = makeArcs('marksB', 2, '#f43f5e'); |
|
var arcsC = makeArcs('marksC', 3, '#f59e0b'); |
|
|
|
// update a vertex's marks: n arcs between side1-dir and D-dir, n between D-dir and side2-dir |
|
function updateMarks(arcs, n, V, P1, D, P2, opacity) { |
|
var a1 = ang(V, P1), aD = ang(V, D), a2 = ang(V, P2); |
|
for (var i = 0; i < n; i++) { |
|
var r = 24 + 6 * i; |
|
arcs[i].setAttribute('d', arcBetween(V, a1, aD, r)); |
|
arcs[n + i].setAttribute('d', arcBetween(V, aD, a2, r)); |
|
} |
|
for (var k = 0; k < 2 * n; k++) arcs[k].setAttribute('opacity', opacity); |
|
} |
|
|
|
/* ---------- rendering ---------- */ |
|
|
|
function curPhase(t) { |
|
for (var i = 4; i >= 0; i--) if (t >= PH[i]) return Math.min(i, 4); |
|
return 0; |
|
} |
|
|
|
function render(t) { |
|
var lp = function (i) { return clamp01((t - PH[i]) / (PH[i + 1] - PH[i])); }; |
|
var D = incenter(); |
|
var XA = lineHit(A, D, B, C); // bisector from A meets BC |
|
var XB = lineHit(B, D, A, C); // bisector from B meets CA |
|
var E = project(D, A, B), F = project(D, B, C), G = project(D, C, A); |
|
var r = dist(D, E); |
|
|
|
// phase 0: sides draw in sequence |
|
var p0 = lp(0); |
|
setLine($('sideAB'), A, lerpPt(A, B, ease(clamp01(p0 * 3)))); |
|
setLine($('sideBC'), B, lerpPt(B, C, ease(clamp01(p0 * 3 - 1)))); |
|
setLine($('sideCA'), C, lerpPt(C, A, ease(clamp01(p0 * 3 - 2)))); |
|
var la = outward(A, 22), lb = outward(B, 22), lc = outward(C, 22); |
|
place($('lblA'), la.x, la.y + 6); place($('lblB'), lb.x, lb.y + 6); place($('lblC'), lc.x, lc.y + 6); |
|
['vA', 'vB', 'vC', 'hitA', 'hitB', 'hitC'].forEach(function (id) { |
|
var V = { vA: A, vB: B, vC: C, hitA: A, hitB: B, hitC: C }[id]; |
|
$(id).setAttribute('cx', V.x); $(id).setAttribute('cy', V.y); |
|
}); |
|
|
|
// phase 1: bisectors from A then B; D pops where they cross |
|
var pA = t > PH[1] ? ease(clamp01(lp(1) * 2)) : 0; |
|
var pB = t > PH[1] ? ease(clamp01(lp(1) * 2 - 1)) : 0; |
|
setLine($('cevA'), A, lerpPt(A, XA, pA)); |
|
setLine($('cevB'), B, lerpPt(B, XB, pB)); |
|
updateMarks(arcsA, 1, A, B, D, C, pA >= 1 ? 1 : 0); |
|
updateMarks(arcsB, 2, B, C, D, A, pB >= 1 ? 1 : 0); |
|
var dFrac = dist(B, D) / dist(B, XB); |
|
var dGrow = clamp01((pB - dFrac) / 0.15); |
|
$('ptD').setAttribute('r', 5.5 * ease(dGrow)); |
|
$('ptD').setAttribute('cx', D.x); $('ptD').setAttribute('cy', D.y); |
|
$('lblD').setAttribute('opacity', dGrow >= 1 ? 1 : 0); |
|
place($('lblD'), D.x + 10, D.y - 9); |
|
|
|
// phase 2: the three perpendiculars DE, DF, DG in sequence |
|
var p2 = t > PH[2] ? lp(2) : 0; |
|
var feet = [ |
|
{ seg: 'perpE', tick: 'tickE', sq: 'sqE', pt: 'ptE', lbl: 'lblE', foot: E, side: [A, B] }, |
|
{ seg: 'perpF', tick: 'tickF', sq: 'sqF', pt: 'ptF', lbl: 'lblF', foot: F, side: [B, C] }, |
|
{ seg: 'perpG', tick: 'tickG', sq: 'sqG', pt: 'ptG', lbl: 'lblG', foot: G, side: [C, A] } |
|
]; |
|
feet.forEach(function (o, i) { |
|
var pp = ease(clamp01(p2 * 3 - i)); |
|
var end = lerpPt(D, o.foot, pp); |
|
setLine($(o.seg), D, end); |
|
var done = pp >= 1; |
|
$(o.pt).setAttribute('cx', o.foot.x); $(o.pt).setAttribute('cy', o.foot.y); |
|
$(o.pt).setAttribute('r', done ? 3.5 : 0); |
|
if (done) { |
|
perpTick($(o.tick), D, o.foot); |
|
var U = o.side[0], V = o.side[1]; |
|
var L = dist(U, V); |
|
var u = { x: (V.x - U.x) / L, y: (V.y - U.y) / L }; |
|
var dd = dist(o.foot, D) || 1; |
|
var v = { x: (D.x - o.foot.x) / dd, y: (D.y - o.foot.y) / dd }; |
|
$(o.sq).setAttribute('points', squarePts(o.foot, u, v, 9)); |
|
var away = { x: o.foot.x + (o.foot.x - D.x) / dd * 16, y: o.foot.y + (o.foot.y - D.y) / dd * 16 }; |
|
place($(o.lbl), away.x, away.y + 5); |
|
} else { |
|
$(o.tick).setAttribute('x1', 0); $(o.tick).setAttribute('y1', 0); |
|
$(o.tick).setAttribute('x2', 0); $(o.tick).setAttribute('y2', 0); |
|
$(o.sq).setAttribute('points', ''); |
|
} |
|
$(o.lbl).setAttribute('opacity', done ? 1 : 0); |
|
}); |
|
var units = (r / 10).toFixed(1); |
|
$('deVal').innerHTML = p2 * 3 >= 1 ? units : '—'; |
|
$('dfVal').innerHTML = p2 * 3 >= 2 ? units : '—'; |
|
$('dgVal').innerHTML = p2 >= 1 ? units : '—'; |
|
|
|
// phase 3: join DC, then mark the two equal halves of ∠C |
|
var p3 = t > PH[3] ? ease(lp(3)) : 0; |
|
setLine($('cevC'), C, lerpPt(C, D, clamp01(p3 * 1.4))); |
|
updateMarks(arcsC, 3, C, B, D, A, clamp01((p3 - 0.7) / 0.3)); |
|
|
|
// phase 4: the incircle and the conclusion |
|
var p4 = t > PH[4] ? ease(lp(4)) : 0; |
|
$('incircle').setAttribute('cx', D.x); $('incircle').setAttribute('cy', D.y); |
|
$('incircle').setAttribute('r', Math.max(r * p4, 0.01)); |
|
$('incircle').setAttribute('opacity', p4); |
|
$('caption').setAttribute('opacity', p4); |
|
|
|
// proof panel highlighting |
|
var ph = curPhase(Math.min(t, 0.999)); |
|
$('stepBadge').innerHTML = 'Proof — step ' + (ph + 1) + ' of 5'; |
|
var items = $('proofList').children; |
|
for (var i = 0; i < items.length; i++) { |
|
items[i].className = i === ph ? 'active' : (i < ph ? 'done' : ''); |
|
} |
|
} |
|
|
|
/* ---------- playback ---------- */ |
|
|
|
function tick(ts) { |
|
if (last === null) last = ts; |
|
var dt = Math.min(ts - last, 100); |
|
last = ts; |
|
if (playing && !recording && !dragging) { |
|
T += dt; |
|
if (T > DUR + HOLD) T = 0; |
|
} |
|
if (!recording) render(Math.min(T / DUR, 1)); |
|
requestAnimationFrame(tick); |
|
} |
|
|
|
function setPlaying(v) { |
|
playing = v; |
|
$('playBtn').innerHTML = v ? '❚❚ Pause' : '▶ Play'; |
|
} |
|
|
|
$('playBtn').addEventListener('click', function () { setPlaying(!playing); }); |
|
$('resetBtn').addEventListener('click', function () { |
|
T = 0; |
|
A = { x: 150, y: 425 }; B = { x: 655, y: 425 }; C = { x: 420, y: 80 }; |
|
setPlaying(true); |
|
}); |
|
|
|
function stepJump(di) { |
|
setPlaying(false); |
|
var t = Math.min(T / DUR, 1); |
|
var i = curPhase(Math.min(t, 0.999)); |
|
var atEnd = t >= PH[i + 1] - 0.005; |
|
var target = di > 0 ? (atEnd ? i + 1 : i) : i - 1; |
|
target = Math.max(0, Math.min(4, target)); |
|
T = (PH[target + 1] - 0.0001) * DUR; |
|
} |
|
$('prevBtn').addEventListener('click', function () { stepJump(-1); }); |
|
$('nextBtn').addEventListener('click', function () { stepJump(1); }); |
|
|
|
/* ---------- vertex dragging ---------- */ |
|
|
|
function svgPoint(evt) { |
|
var p = svg.createSVGPoint(); |
|
p.x = evt.clientX; p.y = evt.clientY; |
|
return p.matrixTransform(svg.getScreenCTM().inverse()); |
|
} |
|
function clampVertex(p) { |
|
return { x: Math.min(Math.max(p.x, 40), 760), y: Math.min(Math.max(p.y, 40), 460) }; |
|
} |
|
[['hitA', function (p) { A = p; }], ['hitB', function (p) { B = p; }], |
|
['hitC', function (p) { C = p; }]].forEach(function (pair) { |
|
var el = $(pair[0]), setV = pair[1]; |
|
el.addEventListener('pointerdown', function (evt) { |
|
dragging = pair[0]; |
|
el.setPointerCapture(evt.pointerId); |
|
evt.preventDefault(); |
|
}); |
|
el.addEventListener('pointermove', function (evt) { |
|
if (dragging !== pair[0]) return; |
|
setV(clampVertex(svgPoint(evt))); |
|
}); |
|
el.addEventListener('pointerup', function () { dragging = null; }); |
|
}); |
|
|
|
/* ---------- GIF export ---------- */ |
|
|
|
GifExport.wireGifButton($('gifBtn'), function () { |
|
var wasPlaying = playing; |
|
recording = true; |
|
playing = false; |
|
return { |
|
svg: svg, |
|
frames: 100, |
|
delayMs: 100, |
|
width: 640, |
|
filename: 'angle-bisector-incenter.gif', |
|
renderFrame: function (i, n) { |
|
render(Math.min(i / (n - 10), 1)); // hold frames on the finished proof |
|
}, |
|
onFinish: function () { |
|
recording = false; |
|
setPlaying(wasPlaying); |
|
} |
|
}; |
|
}); |
|
|
|
render(0); |
|
requestAnimationFrame(tick); |
|
})(); |
|
</script> |
|
</body> |
|
</html> |