Spaces:
Running
Running
Download aries/src/kit.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/kit.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/aries/src/kit.js
-
curl -L -o kit.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/kit.js
20.2 kB
| // ♈ 白羊座 —— 2D 绘图工具箱(全新编写) | |
| export const PI2 = Math.PI * 2; | |
| export const clamp01 = x => x < 0 ? 0 : x > 1 ? 1 : x; | |
| export const mix = (a, b, k) => a + (b - a) * k; | |
| export const span = (t, a, b) => clamp01((t - a) / (b - a)); | |
| export const E = { | |
| sine: k => .5 - .5 * Math.cos(Math.PI * k), | |
| out3: k => 1 - (1 - k) ** 3, | |
| in3: k => k ** 3, | |
| out5: k => 1 - (1 - k) ** 5, | |
| expo: k => k >= 1 ? 1 : 1 - Math.pow(2, -10 * k), | |
| back: k => { const c = 1.9; return 1 + (c + 1) * (k - 1) ** 3 + c * (k - 1) ** 2; }, | |
| bounce: k => { const n = 7.5625, d = 2.75; if (k < 1 / d) return n * k * k; if (k < 2 / d) return n * (k -= 1.5 / d) * k + .75; if (k < 2.5 / d) return n * (k -= 2.25 / d) * k + .9375; return n * (k -= 2.625 / d) * k + .984375; }, | |
| }; | |
| // 进入—停留—离开 | |
| export const life = (t, a, b, fin = .5, fout = .5) => Math.min(E.sine(span(t, a, a + fin)), 1 - E.sine(span(t, b - fout, b))); | |
| export function hash(n) { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); } | |
| export function seeded(seed) { let s = seed | 0 || 1; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return ((s >>> 0) % 1e6) / 1e6; }; } | |
| export const rgba = (c, a = 1) => `rgba(${c[0]},${c[1]},${c[2]},${a})`; | |
| export const GOLD = [255, 205, 110], FIRE = [255, 120, 50], RED = [230, 50, 40], CREAM = [255, 244, 220], ROSE = [255, 150, 160], SKY = [150, 190, 255], LEAF = [140, 230, 140]; | |
| export const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",serif'; | |
| export const SANS = '"Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif'; | |
| export const DISPLAY = '"Cinzel","Noto Serif SC",serif'; | |
| /* ---------- 发光 ---------- */ | |
| const sprites = new Map(); | |
| function sprite(c) { | |
| const k = c.join(','); if (sprites.has(k)) return sprites.get(k); | |
| const N = 128, cv = document.createElement('canvas'); cv.width = cv.height = N; const g = cv.getContext('2d'); | |
| const gr = g.createRadialGradient(N / 2, N / 2, 0, N / 2, N / 2, N / 2); | |
| gr.addColorStop(0, rgba(c, 1)); gr.addColorStop(.15, rgba(c, .6)); gr.addColorStop(.45, rgba(c, .14)); gr.addColorStop(1, rgba(c, 0)); | |
| g.fillStyle = gr; g.fillRect(0, 0, N, N); sprites.set(k, cv); return cv; | |
| } | |
| export function bloom(ctx, x, y, r, c = GOLD, a = 1) { | |
| if (!(a > .002) || !(r > .5)) return; | |
| const op = ctx.globalCompositeOperation, ga = ctx.globalAlpha; | |
| ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = ga * Math.min(a, 1); | |
| ctx.drawImage(sprite(c), x - r, y - r, 2 * r, 2 * r); | |
| ctx.globalCompositeOperation = op; ctx.globalAlpha = ga; | |
| } | |
| export function star(ctx, x, y, r, c = CREAM, a = 1, rays = 1) { | |
| if (!(a > .002)) return; | |
| bloom(ctx, x, y, r * 8, c, a * .8); | |
| const ga = ctx.globalAlpha; ctx.globalAlpha = ga * Math.min(1, a); | |
| ctx.fillStyle = '#fffdf6'; ctx.beginPath(); ctx.arc(x, y, Math.max(.6, r), 0, PI2); ctx.fill(); | |
| if (rays > 0) { | |
| const op = ctx.globalCompositeOperation; ctx.globalCompositeOperation = 'lighter'; | |
| for (const ang of [0, Math.PI / 2]) { | |
| const L = r * 11 * rays, dx = Math.cos(ang) * L, dy = Math.sin(ang) * L; | |
| const g = ctx.createLinearGradient(x - dx, y - dy, x + dx, y + dy); | |
| g.addColorStop(0, rgba(c, 0)); g.addColorStop(.5, rgba(c, .9)); g.addColorStop(1, rgba(c, 0)); | |
| ctx.strokeStyle = g; ctx.lineWidth = Math.max(1, r * .4); ctx.beginPath(); ctx.moveTo(x - dx, y - dy); ctx.lineTo(x + dx, y + dy); ctx.stroke(); | |
| } | |
| ctx.globalCompositeOperation = op; | |
| } | |
| ctx.globalAlpha = ga; | |
| } | |
| /* ---------- 粒子:余烬 / 金粉(解析式,t 的纯函数) ---------- */ | |
| export function embers(ctx, t, W, H, o = {}) { | |
| const n = o.n ?? 80, seed = o.seed ?? 1, c = o.color ?? FIRE, a = o.alpha ?? 1, S = Math.min(W, H) / 1080; | |
| const rise = o.rise ?? 1, spread = o.spread ?? 1; | |
| for (let i = 0; i < n; i++) { | |
| const r1 = hash(seed * 97 + i), r2 = hash(seed * 31 + i * 7.3), r3 = hash(i * 13.7 + seed); | |
| const life = 4 + r3 * 5, ph = (t / life + r1) % 1; | |
| const x = W * (.5 + (r2 - .5) * spread) + Math.sin(t * (.5 + r1) + i) * 40 * S + (o.wind ?? 0) * ph * W * .2; | |
| const y = H * (o.base ?? 1.05) - ph * H * 1.1 * rise; | |
| const fade = Math.sin(ph * Math.PI) * a; | |
| const sz = (1 + r1 * 2.6) * S * (o.size ?? 1); | |
| bloom(ctx, x, y, sz * 7, c, fade * .55); | |
| ctx.globalAlpha = fade; ctx.fillStyle = rgba([255, 235, 190]); ctx.fillRect(x - sz / 2, y - sz / 2, sz, sz); ctx.globalAlpha = 1; | |
| } | |
| } | |
| // 爆散火花:从 (x,y) 在 t0 时刻炸开 | |
| export function burst(ctx, t, t0, x, y, S, o = {}) { | |
| const u = t - t0, dur = o.dur ?? 1.6; if (u < 0 || u > dur) return; | |
| const n = o.n ?? 60, c = o.color ?? GOLD, spd = (o.speed ?? 520) * S; | |
| for (let i = 0; i < n; i++) { | |
| const ang = hash(i * 3.1 + (o.seed ?? 0)) * PI2, v = spd * (.35 + hash(i * 7.7 + (o.seed ?? 0)) * .65); | |
| const k = u / dur, d = v * E.out3(Math.min(1, u / (dur * .8))); | |
| const px = x + Math.cos(ang) * d, py = y + Math.sin(ang) * d + (o.gravity ?? 120) * S * u * u; | |
| const al = (1 - k) ** 1.4; | |
| bloom(ctx, px, py, 9 * S, c, al); | |
| ctx.globalAlpha = al; ctx.fillStyle = '#fff6dc'; ctx.fillRect(px - 1.5 * S, py - 1.5 * S, 3 * S, 3 * S); ctx.globalAlpha = 1; | |
| } | |
| } | |
| /* ---------- 金属渐变 ---------- */ | |
| export function gold(ctx, x0, y0, x1, y1, tone = 0) { | |
| const g = ctx.createLinearGradient(x0, y0, x1, y1); | |
| const stops = tone === 1 // 熔融(锻造时) | |
| ? ['#fff3c4', '#ffb347', '#ff6a1a', '#ffd27a', '#c2410c', '#ffe7a3'] | |
| : ['#fff6d5', '#f7d27a', '#b9822b', '#ffe9a8', '#8a5a19', '#f2c766']; | |
| stops.forEach((c, i) => g.addColorStop(i / (stops.length - 1), c)); | |
| return g; | |
| } | |
| /* ---------- 文字 ---------- */ | |
| export function say(ctx, str, x, y, o = {}) { | |
| const a = o.alpha ?? 1; if (a <= .002) return; | |
| const size = o.size ?? 40; | |
| ctx.save(); ctx.globalAlpha *= Math.min(1, a); | |
| ctx.font = `${o.weight ?? 700} ${size}px ${o.font ?? SERIF}`; | |
| ctx.textAlign = o.align ?? 'center'; ctx.textBaseline = 'middle'; | |
| if ('letterSpacing' in ctx) ctx.letterSpacing = (o.track ?? 0) + 'px'; | |
| if (o.shadow) { ctx.shadowColor = o.shadow; ctx.shadowBlur = o.blur ?? size * .5; } | |
| if (o.stroke) { ctx.strokeStyle = o.stroke; ctx.lineWidth = o.strokeW ?? size * .14; ctx.lineJoin = 'round'; ctx.strokeText(str, x, y); } | |
| if (o.metal) { | |
| const w = ctx.measureText(str).width; const l = ctx.textAlign === 'left' ? x : ctx.textAlign === 'right' ? x - w : x - w / 2; | |
| ctx.fillStyle = gold(ctx, l, y - size * .6, l + w * .3, y + size * .6, o.metal === 'molten' ? 1 : 0); | |
| } else ctx.fillStyle = o.fill ?? '#fff7e6'; | |
| ctx.fillText(str, x, y); | |
| if (o.metal && o.edge !== false) { ctx.shadowBlur = 0; ctx.lineWidth = Math.max(1, size * .02); ctx.strokeStyle = 'rgba(90,50,10,.55)'; ctx.strokeText(str, x, y); } | |
| ctx.restore(); | |
| } | |
| export function measure(ctx, str, size, font = SERIF, weight = 700, track = 0) { | |
| ctx.save(); ctx.font = `${weight} ${size}px ${font}`; if ('letterSpacing' in ctx) ctx.letterSpacing = track + 'px'; | |
| const w = ctx.measureText(str).width; ctx.restore(); return w; | |
| } | |
| // 逐字落下(带缩放与辉光) | |
| export function drop(ctx, str, x, y, k, o = {}) { | |
| if (k <= 0) return; | |
| const chars = [...str], size = o.size ?? 60, tr = o.track ?? 0; | |
| const widths = chars.map(c => measure(ctx, c, size, o.font, o.weight) + tr); | |
| const total = widths.reduce((s, w) => s + w, 0) - tr; | |
| let cx = (o.align === 'left' ? x : x - total / 2); | |
| chars.forEach((c, i) => { | |
| const kk = clamp01(k * (chars.length + 2) - i); | |
| if (kk > 0) { | |
| const e = E.back(kk); const sc = mix(1.8, 1, E.out3(kk)); | |
| ctx.save(); ctx.translate(cx + (widths[i] - tr) / 2, y - (1 - E.out3(kk)) * size * .6); ctx.scale(sc, sc); | |
| say(ctx, c, 0, 0, { ...o, align: 'center', track: 0, alpha: (o.alpha ?? 1) * clamp01(kk * 1.5) }); | |
| ctx.restore(); void e; | |
| } | |
| cx += widths[i]; | |
| }); | |
| } | |
| export function wrap(ctx, str, maxW, size, font = SANS, weight = 600) { | |
| ctx.save(); ctx.font = `${weight} ${size}px ${font}`; | |
| const lines = []; let cur = ''; | |
| for (const ch of str) { if (ctx.measureText(cur + ch).width > maxW && cur) { lines.push(cur); cur = ch; } else cur += ch; } | |
| if (cur) lines.push(cur); ctx.restore(); | |
| if (lines.length === 2) { | |
| const all = [...lines.join('')], n = all.length, PUN = ',。、!?;:,.!?;:”」)'; | |
| let h = Math.ceil(n / 2), best = -1; | |
| for (let i = 1; i < n - 1; i++) if (PUN.includes(all[i - 1]) && Math.abs(i - n / 2) <= n * .22 && (best < 0 || Math.abs(i - n / 2) < Math.abs(best - n / 2))) best = i; | |
| if (best > 0) h = best; else while (h < n - 1 && PUN.includes(all[h])) h++; | |
| return [all.slice(0, h).join(''), all.slice(h).join('')]; | |
| } | |
| return lines; | |
| } | |
| /* ---------- 形状 ---------- */ | |
| export function rr(ctx, x, y, w, h, r) { | |
| r = Math.min(r, w / 2, h / 2); ctx.beginPath(); | |
| ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); | |
| } | |
| export function card(ctx, x, y, w, h, o = {}) { | |
| const a = o.alpha ?? 1; if (a <= .002) return; | |
| ctx.save(); ctx.globalAlpha *= a; | |
| rr(ctx, x, y, w, h, o.r ?? 18); | |
| const g = ctx.createLinearGradient(x, y, x, y + h); g.addColorStop(0, o.top ?? 'rgba(60,18,20,.72)'); g.addColorStop(1, o.bottom ?? 'rgba(20,6,10,.72)'); | |
| ctx.fillStyle = g; ctx.fill(); | |
| ctx.lineWidth = o.lw ?? 2; ctx.strokeStyle = gold(ctx, x, y, x + w, y + h); ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| // 部分描边:按长度比例绘制 Path2D | |
| export function trace(ctx, path, len, k, o = {}) { | |
| if (k <= 0) return; ctx.save(); | |
| if (k < 1) { ctx.setLineDash([len * k, len * 2]); } | |
| ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = o.width ?? 4; ctx.strokeStyle = o.color ?? '#ffd88a'; | |
| if (o.glow) { ctx.shadowColor = o.glow; ctx.shadowBlur = o.blur ?? 16; } | |
| ctx.stroke(path); ctx.restore(); | |
| } | |
| /* ---------- 羊角黄金螺线 ---------- */ | |
| // 以 (cx,cy) 为螺线中心,从外端向内卷;dir=±1 决定卷向;返回外端点 | |
| export function hornPath(cx, cy, r0, turns, dir, rot = 0, w0 = .22) { | |
| const N = 90, b = Math.log(1 / 7) / (turns * PI2); // r 从 r0 缩到 r0/7 | |
| const outer = [], inner = []; | |
| for (let i = 0; i <= N; i++) { | |
| const th = i / N * turns * PI2; const r = r0 * Math.exp(b * th); | |
| const ang = rot + dir * th; const w = r0 * w0 * (1 - i / N * .85); | |
| const x = cx + Math.cos(ang) * r, y = cy + Math.sin(ang) * r; | |
| outer.push([cx + Math.cos(ang) * (r + w / 2), cy + Math.sin(ang) * (r + w / 2)]); | |
| inner.push([cx + Math.cos(ang) * (r - w / 2), cy + Math.sin(ang) * (r - w / 2)]); | |
| void x; void y; | |
| } | |
| const p = new Path2D(); p.moveTo(...outer[0]); outer.forEach(q => p.lineTo(...q)); for (let i = inner.length - 1; i >= 0; i--) p.lineTo(...inner[i]); p.closePath(); | |
| const spine = new Path2D(); for (let i = 0; i <= N; i++) { const th = i / N * turns * PI2, r = r0 * Math.exp(b * th), ang = rot + dir * th; i ? spine.lineTo(cx + Math.cos(ang) * r, cy + Math.sin(ang) * r) : spine.moveTo(cx + Math.cos(ang) * r, cy + Math.sin(ang) * r); } | |
| const ridges = []; for (let i = 4; i < N; i += 5) ridges.push([outer[i], inner[i]]); | |
| return { shape: p, spine, ridges, len: r0 * turns * PI2 * .55 }; | |
| } | |
| /* ---------- 正面公羊头(徽章式) ---------- */ | |
| export function ramHead(ctx, x, y, s, o = {}) { | |
| const k = o.k ?? 1, a = o.alpha ?? 1; if (a <= 0 || k <= 0) return; | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s, s); ctx.rotate(o.tilt ?? 0); ctx.globalAlpha *= a; | |
| const tone = o.molten ? 1 : 0; | |
| // 角(先描线后填充) | |
| for (const d of [-1, 1]) { | |
| const h = hornPath(d * 92, -40, 78, 1.15, d, d > 0 ? -Math.PI * .95 : -Math.PI * .05, .5); | |
| const fill = span(k, .35, .7); | |
| if (fill > 0) { ctx.save(); ctx.globalAlpha *= fill; ctx.fillStyle = gold(ctx, -170, -140, 170, 60, tone); ctx.shadowColor = 'rgba(255,170,60,.7)'; ctx.shadowBlur = 24; ctx.fill(h.shape); ctx.restore(); | |
| ctx.save(); ctx.globalAlpha *= fill * .55; ctx.strokeStyle = 'rgba(110,60,10,.9)'; ctx.lineWidth = 2.4; for (const [p, q] of h.ridges) { ctx.beginPath(); ctx.moveTo(...p); ctx.lineTo(...q); ctx.stroke(); } ctx.restore(); } | |
| trace(ctx, h.spine, h.len * 1.9, span(k, 0, .45), { width: 5, color: '#ffe3a0', glow: 'rgba(255,190,90,.9)' }); | |
| } | |
| // 头 | |
| const face = new Path2D(); | |
| face.moveTo(0, -95); face.bezierCurveTo(48, -95, 62, -50, 52, -5); face.bezierCurveTo(44, 40, 30, 95, 0, 120); | |
| face.bezierCurveTo(-30, 95, -44, 40, -52, -5); face.bezierCurveTo(-62, -50, -48, -95, 0, -95); face.closePath(); | |
| const fk = span(k, .25, .65); | |
| if (fk > 0) { | |
| ctx.save(); ctx.globalAlpha *= fk; | |
| ctx.fillStyle = gold(ctx, -60, -100, 60, 120, tone); ctx.shadowColor = 'rgba(255,160,60,.6)'; ctx.shadowBlur = 30; ctx.fill(face); ctx.shadowBlur = 0; | |
| // 耳 | |
| for (const d of [-1, 1]) { ctx.beginPath(); ctx.ellipse(d * 70, -10, 34, 12, d * .45, 0, PI2); ctx.fill(); ctx.strokeStyle = 'rgba(110,60,10,.6)'; ctx.lineWidth = 2; ctx.stroke(); } | |
| ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(100,55,10,.75)'; ctx.stroke(face); | |
| // 额毛卷 | |
| ctx.lineWidth = 2.2; for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.arc(-28 + i * 14, -70 + (i % 2) * 8, 9, Math.PI * .1, Math.PI * 1.4); ctx.stroke(); } | |
| // 鼻梁 / 鼻孔 / 口 | |
| ctx.beginPath(); ctx.moveTo(-14, 0); ctx.quadraticCurveTo(0, 30, -10, 88); ctx.moveTo(14, 0); ctx.quadraticCurveTo(0, 30, 10, 88); ctx.stroke(); | |
| ctx.beginPath(); ctx.ellipse(-12, 96, 6, 10, .3, 0, PI2); ctx.ellipse(12, 96, 6, 10, -.3, 0, PI2); ctx.fillStyle = 'rgba(90,45,8,.8)'; ctx.fill(); | |
| ctx.restore(); | |
| // 眼睛(发光) | |
| const eye = span(k, .6, .9) * (o.eye ?? 1); | |
| for (const d of [-1, 1]) { ctx.save(); ctx.globalAlpha *= fk; ctx.fillStyle = '#3a1d05'; ctx.beginPath(); ctx.ellipse(d * 27, -22, 11, 6, d * -.25, 0, PI2); ctx.fill(); ctx.restore(); bloom(ctx, d * 27, -22, 26, o.eyeColor ?? [255, 120, 40], eye); } | |
| } | |
| ctx.restore(); | |
| } | |
| /* ---------- 侧面公羊(飞行 / 奔跑) ---------- */ | |
| export function ramSide(ctx, x, y, s, o = {}) { | |
| const a = o.alpha ?? 1; if (a <= 0) return; | |
| const t = o.t ?? 0, gait = o.gait ?? 0, fly = o.fly ?? 0; | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s * (o.flip ? -1 : 1), s); ctx.globalAlpha *= a; | |
| const body = new Path2D(); | |
| body.moveTo(70, -20); body.bezierCurveTo(40, -62, -40, -62, -82, -34); body.bezierCurveTo(-112, -14, -100, 30, -76, 34); | |
| body.bezierCurveTo(-30, 46, 30, 46, 64, 30); body.bezierCurveTo(88, 18, 88, -4, 70, -20); body.closePath(); | |
| // 腿 | |
| const leg = (bx, by, ph, back) => { | |
| const sw = gait ? Math.sin(t * 9 + ph) * .55 : 0; const tuck = fly; | |
| const a1 = mix(sw + (back ? .1 : -.1), back ? .9 : -1.0, tuck), a2 = mix(Math.max(0, -sw) * 1.1 + .1, back ? -1.4 : 1.4, tuck); | |
| ctx.save(); ctx.translate(bx, by); ctx.rotate(a1); | |
| ctx.lineCap = 'round'; ctx.strokeStyle = o.legColor ?? '#c48f3a'; ctx.lineWidth = 11; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, 42); ctx.stroke(); | |
| ctx.translate(0, 42); ctx.rotate(a2); ctx.lineWidth = 8; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, 34); ctx.stroke(); | |
| ctx.fillStyle = '#4a2a08'; ctx.fillRect(-6, 30, 12, 8); ctx.restore(); | |
| }; | |
| leg(-62, 22, Math.PI, true); leg(46, 22, 0, false); | |
| // 身体(金羊毛) | |
| ctx.save(); ctx.fillStyle = gold(ctx, -100, -60, 90, 50, o.molten ? 1 : 0); ctx.shadowColor = 'rgba(255,190,80,.8)'; ctx.shadowBlur = 30 * (o.shine ?? 1); ctx.fill(body); ctx.restore(); | |
| ctx.save(); ctx.clip(body); ctx.strokeStyle = 'rgba(120,70,10,.45)'; ctx.lineWidth = 2.4; | |
| for (let i = 0; i < 26; i++) { const cx = -100 + (i * 37) % 190, cy = -50 + Math.floor(i * 37 / 190) * 22 + (i % 3) * 4; ctx.beginPath(); ctx.arc(cx, cy, 10, Math.PI * .2, Math.PI * 1.7); ctx.stroke(); } | |
| ctx.restore(); | |
| leg(-44, 26, Math.PI * .5, true); leg(60, 24, Math.PI * 1.5, false); | |
| // 颈 + 头 | |
| const g2 = gold(ctx, 40, -120, 160, -20, o.molten ? 1 : 0); | |
| ctx.save(); ctx.fillStyle = g2; ctx.strokeStyle = 'rgba(110,60,10,.6)'; ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.moveTo(44, -40); ctx.bezierCurveTo(62, -60, 80, -78, 98, -86); ctx.lineTo(120, -70); ctx.bezierCurveTo(104, -56, 86, -30, 74, -6); ctx.closePath(); ctx.fill(); | |
| ctx.beginPath(); ctx.ellipse(122, -78, 30, 17, .5, 0, PI2); ctx.fill(); ctx.stroke(); // 头 | |
| ctx.beginPath(); ctx.ellipse(141, -60, 13, 11, .5, 0, PI2); ctx.fill(); ctx.stroke(); // 口鼻 | |
| ctx.beginPath(); ctx.ellipse(100, -78, 16, 6, -.5, 0, PI2); ctx.fill(); ctx.stroke(); // 耳 | |
| ctx.restore(); | |
| ctx.fillStyle = '#3a1d05'; ctx.beginPath(); ctx.arc(126, -84, 3.6, 0, PI2); ctx.fill(); bloom(ctx, 126, -84, 9, [255, 150, 60], .8); | |
| // 角 | |
| const h = hornPath(110, -88, 28, 1.2, 1, -Math.PI * .95, .6); | |
| ctx.save(); ctx.fillStyle = gold(ctx, 80, -130, 150, -60); ctx.shadowColor = 'rgba(255,170,60,.7)'; ctx.shadowBlur = 14; ctx.fill(h.shape); ctx.restore(); | |
| ctx.save(); ctx.strokeStyle = 'rgba(110,60,10,.7)'; ctx.lineWidth = 1.6; for (const [p, q] of h.ridges) { ctx.beginPath(); ctx.moveTo(...p); ctx.lineTo(...q); ctx.stroke(); } ctx.restore(); | |
| ctx.restore(); | |
| } | |
| /* ---------- 符号 ---------- */ | |
| export function ariesGlyph(ctx, x, y, s, k = 1, o = {}) { | |
| if (k <= 0) return; | |
| const p = new Path2D(); | |
| p.moveTo(0, 90); p.lineTo(0, -10); | |
| p.bezierCurveTo(0, -70, -30, -95, -60, -88); p.bezierCurveTo(-95, -80, -98, -30, -70, -18); p.bezierCurveTo(-55, -12, -42, -24, -44, -40); | |
| p.moveTo(0, -10); p.bezierCurveTo(0, -70, 30, -95, 60, -88); p.bezierCurveTo(95, -80, 98, -30, 70, -18); p.bezierCurveTo(55, -12, 42, -24, 44, -40); | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s, s); | |
| trace(ctx, p, 760, k, { width: o.width ?? 14, color: o.color ?? gold(ctx, -90, -90, 90, 90), glow: o.glow ?? 'rgba(255,170,70,.95)', blur: o.blur ?? 26 }); | |
| ctx.restore(); | |
| } | |
| export function marsGlyph(ctx, x, y, s, k = 1, col = '#ff8a6a') { | |
| if (k <= 0) return; | |
| const p = new Path2D(); p.arc(-10, 12, 34, -Math.PI * .25, Math.PI * 1.75); p.moveTo(14, -12); p.lineTo(48, -46); p.moveTo(22, -46); p.lineTo(48, -46); p.lineTo(48, -20); | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s, s); trace(ctx, p, 360, k, { width: 8, color: col, glow: 'rgba(255,90,60,.9)' }); ctx.restore(); | |
| } | |
| // 月桂枝 | |
| export function laurel(ctx, x, y, s, side, k = 1, o = {}) { | |
| if (k <= 0) return; | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s * side, s); | |
| const n = 9; ctx.strokeStyle = o.stem ?? '#d9a648'; ctx.lineWidth = 4; ctx.lineCap = 'round'; | |
| ctx.beginPath(); for (let i = 0; i <= 40 * k; i++) { const th = Math.PI * (.62 + i / 40 * .78); const px = Math.cos(th) * 150, py = Math.sin(th) * 150; i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); } ctx.stroke(); | |
| for (let i = 0; i < n; i++) { | |
| const kk = clamp01(k * n - i); if (kk <= 0) continue; | |
| const th = Math.PI * (.66 + i / n * .74), px = Math.cos(th) * 150, py = Math.sin(th) * 150; | |
| for (const d of [-1, 1]) { | |
| ctx.save(); ctx.translate(px, py); ctx.rotate(th + Math.PI / 2 + d * .55); ctx.scale(kk, kk); | |
| ctx.fillStyle = gold(ctx, -10, -30, 10, 0); ctx.beginPath(); ctx.ellipse(0, -18, 8, 20, 0, 0, PI2); ctx.fill(); | |
| ctx.strokeStyle = 'rgba(100,60,10,.6)'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(0, -2); ctx.lineTo(0, -34); ctx.stroke(); ctx.restore(); | |
| } | |
| } | |
| ctx.restore(); | |
| } | |
| // 火焰(程序化,t 驱动) | |
| export function flame(ctx, x, y, s, t, a = 1, hue = 0) { | |
| if (a <= 0) return; | |
| ctx.save(); ctx.translate(x, y); ctx.scale(s, s); ctx.globalAlpha *= a; | |
| const layers = [[1, hue ? '#ff5a3c' : '#ff4d1a'], [.72, '#ff9a2e'], [.46, '#ffd66b'], [.24, '#fff4c9']]; | |
| for (const [k, c] of layers) { | |
| const w = 60 * k, h = 160 * k, sw = Math.sin(t * 6 + k * 5) * 8 * k, sw2 = Math.sin(t * 9 + k * 3) * 6 * k; | |
| ctx.beginPath(); ctx.moveTo(0, 0); | |
| ctx.bezierCurveTo(-w * 1.1, -h * .1, -w * .6 + sw, -h * .55, sw2, -h); | |
| ctx.bezierCurveTo(w * .6 + sw, -h * .55, w * 1.1, -h * .1, 0, 0); ctx.closePath(); | |
| ctx.fillStyle = c; ctx.shadowColor = c; ctx.shadowBlur = 30 * k; ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| // 天球投影(赤经 h, 赤纬 °)→ 相对“度”坐标(东在左) | |
| export const proj = (ra, dec, ra0, dec0) => [-(ra - ra0) * 15 * Math.cos(dec * Math.PI / 180), -(dec - dec0)]; | |