File size: 9,070 Bytes
a7ffb22 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import { esc } from "./math.js";
// The sky look's pictures. The owl is mathly1's (static/math-sky.js), coloured by the --sky-owl* tokens in app.css.
export const owl = (width = 80, cls = "") => `<svg class="sky-owl ${cls}" width="${width}" height="${Math.round(width * 1.04)}" viewBox="0 0 100 104" aria-hidden="true">
<path d="M20 34 26 6l17 18q7-3 14 0L74 6l6 28q12 22 4 44-9 24-34 24T16 78q-8-22 4-44Z" class="o-b"/>
<path d="M16 62q-8 14 2 28 4-10 4-22Z M84 62q8 14-2 28-4-10-4-22Z" class="o-b" opacity=".75"/>
<ellipse cx="50" cy="74" rx="22" ry="23" class="o-belly"/>
<path d="M40 70q3 3 6 0M54 70q3 3 6 0M44 80q3 3 6 0M50 80q3 3 6 0" class="o-feather"/>
<circle cx="35" cy="44" r="13" class="o-eye"/><circle cx="65" cy="44" r="13" class="o-eye"/>
<circle cx="37" cy="45" r="6.5" class="o-pupil"/><circle cx="63" cy="45" r="6.5" class="o-pupil"/>
<circle cx="39" cy="42.5" r="2" class="o-glint"/><circle cx="65" cy="42.5" r="2" class="o-glint"/>
<ellipse class="o-lid" cx="35" cy="44" rx="13.5" ry="13.5"/><ellipse class="o-lid" cx="65" cy="44" rx="13.5" ry="13.5"/>
<circle cx="35" cy="44" r="15.5" class="o-glass"/><circle cx="65" cy="44" r="15.5" class="o-glass"/>
<path d="M48 42q2-2 4 0" class="o-glass"/>
<path d="M45 56h10l-5 9Z" class="o-beak"/>
<path d="M38 99l-3 4M44 100v4M56 100v4M62 99l3 4" class="o-feet"/></svg>`;
/** The four-pointed star every skill is (mathly1's spark). Its colour and fill come from CSS (state classes). */
export const spark = (cls = "") => `<svg viewBox="0 0 24 24" class="${cls}" aria-hidden="true"><path d="M12 1.5C13.1 9 15 10.9 22.5 12C15 13.1 13.1 15 12 22.5C10.9 15 9 13.1 1.5 12C9 10.9 10.9 9 12 1.5Z"/></svg>`;
/** The arcs that join a lesson's stars in a ring (from mathly1's orbitRing). One path per neighbouring pair, clockwise from the top;
* an arc is "on" once the star it leaves has been started. The closing arc from the last star back to the first stays faint:
* the ring has a start and an end. Drawn in a 100x100 box that is stretched to the ring's size, so strokes do not scale. */
export function ringSvg(states) {
const n = states.length;
if (n < 2) return "";
const angle = i => (-90 + 360 * i / n) * Math.PI / 180;
const gap = Math.min(0.32, Math.PI / n * 0.55); // room left around each star (radians)
const point = t => [50 + 50 * Math.cos(t), 50 + 50 * Math.sin(t)];
const f = v => v.toFixed(2);
const arc = (from, to, cls) => {
const [x0, y0] = point(from), [x1, y1] = point(to);
return `<path class="${cls}" d="M${f(x0)} ${f(y0)}A50 50 0 0 1 ${f(x1)} ${f(y1)}"/>`;
};
const arcs = states.slice(0, -1).map((state, k) => arc(angle(k) + gap, angle(k + 1) - gap, state === "not_started" ? "" : "on")).join("");
const back = n > 2 ? arc(angle(n - 1) + gap, angle(n) - gap, "back") : "";
return `<svg class="ring" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${back}${arcs}</svg>`;
}
/* ---------- constellations and comets (ported from mathly1) ---------- */
const SPARK = "M12 1.5C13.1 9 15 10.9 22.5 12C15 13.1 13.1 15 12 22.5C10.9 15 9 13.1 1.5 12C9 10.9 10.9 9 12 1.5Z";
const curve=(f,n=48)=>Array.from({length:n+1},(_,i)=>f(i/n));
const SHAPES={
t:{name:"ืืงืขืจื",aspect:.9,pts:curve(s=>[1-s,1-(2*s-1)**2])},
t2:{name:"ืืืืจืืืช",aspect:.75,corners:1,pts:[[1,1],[1,.5],[.5,.5],[.5,0],[0,0]]},
k:{name:"ืืืืชืื",aspect:1.15,closed:1,corners:1,pts:[[.5,0],[1,.36],[.5,1],[0,.36]],inner:[[[.5,0],[.5,1]],[[0,.36],[1,.36]]]},
i:{name:"ืืืฉืืืฉ",aspect:.9,closed:1,corners:1,pts:[[.5,0],[1,1],[0,1]],inner:[[[.5,0],[.5,1]]]},
q:{name:"ืืงืฉืช",aspect:.75,pts:curve(s=>[1-s,(2*s-1)**2])},
q2:{name:"ืืคืจืืืื",aspect:.85,pts:curve(s=>[1-s,1-(2*s-1)**2]),inner:[[[.5,0],[.5,1]]]},
a:{name:"ืืืืืืช",aspect:.7,closed:1,corners:1,pts:[[1,0],[1,1],[0,1]]},
l:{name:"ืืืฉืืืฉ ืืืฉืจ",aspect:.75,closed:1,corners:1,pts:[[1,0],[1,1],[0,1]]},
z:{name:"ืืืจืคื",aspect:.6,closed:1,corners:1,pts:[[.72,0],[1,1],[0,1],[.28,0]]},
z2:{name:"ืืืจืคื ืืืฉืจ",aspect:.6,closed:1,corners:1,pts:[[.6,0],[1,1],[0,1],[0,0]]},
e:{name:"ืืืืขื",aspect:.55,pts:curve(s=>[1-s,1-Math.sin(Math.PI*s)])},
v:{name:"ืืืฅ",aspect:.8,corners:1,pts:[[0,0],[1,.5],[0,1]]},
g:{name:"ืืืงืืืืืช",aspect:.6,closed:1,corners:1,pts:[[1,0],[.72,1],[0,1],[.28,0]]},
r:{name:"ืืืืคืจืืืื",aspect:.9,pts:curve(s=>{const x=.06+.94*(1-s);return [x,Math.min(1,.06/x)]})},
m:{name:"ืืืืื",aspect:.62,closed:1,corners:1,pts:[[1,0],[1,1],[0,1],[0,0]],inner:[[[1,0],[0,1]]]},
h:{name:"ืืงืืืืื",aspect:1.1,closed:1,corners:1,pts:[[.5,0],[1,.25],[1,.75],[.5,1],[0,.75],[0,.25]],inner:[[[.5,.5],[.5,1]],[[.5,.5],[0,.25]],[[.5,.5],[1,.25]]]},
s:{name:"ืืืคืืฉ",aspect:.75,pts:curve(s=>[1-s,(2*s-1)**2]),inner:[[[1,.12],[0,.88]]]},
n:{name:"ืืื",aspect:.5,pts:curve(s=>[1-s,.5-.5*Math.sin(2*Math.PI*s)])},
b:{name:"ืืืขืืื",aspect:1.25,closed:1,corners:1,pts:[[.5,0],[1,.5],[.5,1],[0,.5]],inner:[[[.5,0],[.5,1]],[[0,.5],[1,.5]]]},
c:{name:"ืืจืืืืข",aspect:1,closed:1,corners:1,pts:[[1,0],[1,1],[0,1],[0,0]],inner:[[[1,0],[0,1]],[[0,0],[1,1]]]},
o:{name:"ืืชืืืื",aspect:.8,closed:1,corners:1,pts:[[1,0],[1,1],[0,1]],inner:[[[1,1],[.5,.5]]]},
p:{name:"ืืจืงืื",aspect:1,pts:curve(s=>{const x=1-s;return [x,1-(Math.pow(40,x)-1)/39]})},
w:{name:"ืืืจืง",aspect:.6,corners:1,pts:[[1,1],[.75,0],[.5,.75],[.25,0],[0,1]]},
d:{name:"ืืืฉืืืฉืื ืืืืืื",aspect:.9,closed:1,corners:1,pts:[[.5,0],[1,1],[0,1]],inner:[[[.25,.5],[.75,.5]]]},
};
const FALLBACK={aspect:.7,corners:1,pts:[[1,.15],[.68,.85],[.36,.1],[0,.75]]};
const W=360, X0=60, X1=300, TOP=52, BOTTOM=64;
const lerp=(a,b,t)=>[a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t];
const dist=(a,b)=>Math.hypot(b[0]-a[0],b[1]-a[1]);
function segments(pts,closed){ const out=[]; for(let i=0;i<pts.length-1;i++) out.push([pts[i],pts[i+1]]); if(closed) out.push([pts[pts.length-1],pts[0]]); return out; }
function along(pts,closed,n){
const segs=segments(pts,closed), L=segs.reduce((s,[a,b])=>s+dist(a,b),0);
const at=d=>{ for(const [a,b] of segs){ const l=dist(a,b); if(d<=l+1e-9) return lerp(a,b,l?d/l:0); d-=l; } return segs[segs.length-1][1]; };
if(n<=0) return [];
if(closed) return Array.from({length:n},(_,i)=>at(L*i/n));
if(n===1) return [at(L/2)];
return Array.from({length:n},(_,i)=>at(L*i/(n-1)));
}
function onCorners(pts,closed,n){
const segs=segments(pts,closed), extra=segs.map(()=>0);
for(let k=0;k<n-pts.length;k++){ let best=0; segs.forEach(([a,b],i)=>{ if(dist(a,b)/(extra[i]+1)>dist(...segs[best])/(extra[best]+1)) best=i; }); extra[best]++; }
const out=[];
segs.forEach(([a,b],i)=>{ out.push(a); for(let j=1;j<=extra[i];j++) out.push(lerp(a,b,j/(extra[i]+1))); });
if(!closed) out.push(pts[pts.length-1]);
return out;
}
/* The drawing of unit `id` with `n` stars, in a box W wide and H high: the stars in the path's order, the outline's
segments, and the inner lines. A unit with many stars is drawn taller so that they keep apart. */
export function shape(id,n,name){
const S=SHAPES[id]||{...FALLBACK,name};
const w=X1-X0, h=Math.round(Math.max(w*S.aspect,n>6?(n-1)*36:0)), H=TOP+h+BOTTOM;
const map=([x,y])=>[X0+x*w,TOP+y*h];
const pts=S.pts.map(map);
const stars=(S.corners&&n>=pts.length?onCorners(pts,!!S.closed,n):along(pts,!!S.closed,n));
return {name:S.name||name,H,stars,lines:segments(pts,!!S.closed),inner:(S.inner||[]).map(l=>l.map(map))};
}
/** A completed lesson's constellation: its outline and inner lines, and its stars as small gold sparks, in the shape its lesson id gives
* (a zig-zag when none is defined). Stars are numbered in the lesson's order for the screen reader only. */
export function constellationSvg(lessonId, name, count) {
const shp = shape(lessonId, count, name);
const line = ([a, b]) => `<line x1="${a[0].toFixed(1)}" y1="${a[1].toFixed(1)}" x2="${b[0].toFixed(1)}" y2="${b[1].toFixed(1)}"/>`;
const stars = shp.stars.map(([x, y]) => `<path d="${SPARK}" transform="translate(${(x - 20).toFixed(1)} ${(y - 20).toFixed(1)}) scale(${(20 / 12).toFixed(3)})"/>`).join("");
return `<svg class="con" viewBox="34 22 292 ${shp.H - 56}" role="img" aria-label="ืงืืืฆืช ืืืืืืื ${name}: ${count} ืืืืืื">${[...shp.lines, ...shp.inner].map(line).join("")}${stars}</svg>`;
}
/** The comet that crosses the screen: a gold tail and a spark. */
export const cometSvg = () => `<svg viewBox="0 0 120 56" aria-hidden="true"><defs><linearGradient id="cometTail" x1="0" x2="1"><stop offset="0" stop-color="#F7C948" stop-opacity="0"/><stop offset="1" stop-color="#F7C948"/></linearGradient></defs>
<path d="M2 28 98 20 98 36Z" fill="url(#cometTail)"/><path d="${SPARK}" transform="translate(84 10) scale(1.5)" fill="#EAF0FF"/></svg>`;
/** A caught comet's card: its symbol and name (the fact is written under it by the screen). */
export const cometCard = c => `<div class="comet-card"><span class="sym" dir="ltr">${esc(c.sym)}</span><b>${esc(c.name)}</b></div>`;
|