@kernel.chat/kbot 3.83.0 → 3.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-protocol.d.ts +8 -0
- package/dist/agent-protocol.js +25 -0
- package/dist/agent.js +96 -4
- package/dist/collective.d.ts +13 -2
- package/dist/collective.js +50 -5
- package/dist/dream.d.ts +17 -2
- package/dist/dream.js +39 -8
- package/dist/free-energy.d.ts +31 -0
- package/dist/free-energy.js +55 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/kbot-browser.d.ts +75 -0
- package/dist/tools/kbot-browser.js +1049 -0
- package/dist/tools/render-engine.d.ts +109 -0
- package/dist/tools/render-engine.js +647 -0
- package/dist/tools/stream-renderer.js +48 -3
- package/package.json +1 -1
|
@@ -478,6 +478,141 @@ export function renderParticles(ctx, particles) {
|
|
|
478
478
|
ctx.globalAlpha = 1;
|
|
479
479
|
ctx.restore();
|
|
480
480
|
}
|
|
481
|
+
// ─── 3b. POSITION-BASED PARTICLE DYNAMICS (PBD UPGRADE) ─────────
|
|
482
|
+
/**
|
|
483
|
+
* Tick particles using Verlet integration (PBD style) instead of simple velocity.
|
|
484
|
+
* Adds floor constraints with realistic bounce and attractor constraints for orbital particles.
|
|
485
|
+
*/
|
|
486
|
+
export function tickParticlesPBD(particles, groundLevel = 480, attractorX = 0, attractorY = 0) {
|
|
487
|
+
const dt = 1; // timestep (frame-based)
|
|
488
|
+
return particles.filter(p => {
|
|
489
|
+
p.life--;
|
|
490
|
+
if (p.life <= 0)
|
|
491
|
+
return false;
|
|
492
|
+
// Reconstruct previous position from velocity
|
|
493
|
+
const prevX = p.x - p.vx;
|
|
494
|
+
const prevY = p.y - p.vy;
|
|
495
|
+
switch (p.type) {
|
|
496
|
+
case 'spark': {
|
|
497
|
+
// Store trail
|
|
498
|
+
if (p.trail) {
|
|
499
|
+
p.trail.push({ x: p.x, y: p.y });
|
|
500
|
+
if (p.trail.length > 3)
|
|
501
|
+
p.trail.shift();
|
|
502
|
+
}
|
|
503
|
+
// Apply gravity force (Verlet: position += accel * dt^2)
|
|
504
|
+
p.y += (p.gravity ?? 0.3) * dt * dt;
|
|
505
|
+
p.x += p.vx;
|
|
506
|
+
p.y += p.vy;
|
|
507
|
+
// Constraint: floor collision with bounce
|
|
508
|
+
if (p.y > groundLevel && p.vy > 0) {
|
|
509
|
+
p.y = groundLevel;
|
|
510
|
+
const vyFromPos = p.y - prevY;
|
|
511
|
+
p.vy = -Math.abs(vyFromPos) * 0.4;
|
|
512
|
+
if (Math.abs(p.vy) < 0.5)
|
|
513
|
+
p.vy = 0;
|
|
514
|
+
}
|
|
515
|
+
// Derive velocity from position change (PBD)
|
|
516
|
+
p.vx = p.x - prevX;
|
|
517
|
+
p.vy = p.y - prevY;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
case 'fire': {
|
|
521
|
+
p.vy += p.gravity ?? -0.2;
|
|
522
|
+
p.vx = Math.sin(p.life * 0.3) * 0.5;
|
|
523
|
+
p.x += p.vx;
|
|
524
|
+
p.y += p.vy;
|
|
525
|
+
p.size = Math.max(1, p.size - 0.05);
|
|
526
|
+
const ratio = p.life / p.maxLife;
|
|
527
|
+
if (ratio > 0.7)
|
|
528
|
+
p.color = '#f0c040';
|
|
529
|
+
else if (ratio > 0.4)
|
|
530
|
+
p.color = '#ff6600';
|
|
531
|
+
else if (ratio > 0.2)
|
|
532
|
+
p.color = '#cc2200';
|
|
533
|
+
else
|
|
534
|
+
p.color = '#661100';
|
|
535
|
+
// Floor constraint for fire (fire rises, but embers fall)
|
|
536
|
+
if (p.y > groundLevel) {
|
|
537
|
+
p.y = groundLevel;
|
|
538
|
+
p.vy = -Math.abs(p.vy) * 0.2;
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 'magic': {
|
|
543
|
+
if (p.cx !== undefined && p.cy !== undefined && p.orbitRadius !== undefined && p.orbitPhase !== undefined) {
|
|
544
|
+
p.orbitPhase += 0.15;
|
|
545
|
+
p.x = p.cx + Math.cos(p.orbitPhase) * p.orbitRadius;
|
|
546
|
+
p.y = p.cy + Math.sin(p.orbitPhase) * p.orbitRadius;
|
|
547
|
+
}
|
|
548
|
+
// Attractor constraint: robot core pulls nearby magic particles
|
|
549
|
+
const dx = attractorX - p.x;
|
|
550
|
+
const dy = attractorY - p.y;
|
|
551
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
552
|
+
if (dist < 200 && dist > 0) {
|
|
553
|
+
p.x += dx * 0.02;
|
|
554
|
+
p.y += dy * 0.02;
|
|
555
|
+
}
|
|
556
|
+
// Rainbow cycle
|
|
557
|
+
{
|
|
558
|
+
const rainbow = ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'];
|
|
559
|
+
p.color = rainbow[Math.floor((p.maxLife - p.life) * 0.3) % rainbow.length];
|
|
560
|
+
}
|
|
561
|
+
// Derive velocity from position change (PBD)
|
|
562
|
+
p.vx = p.x - prevX;
|
|
563
|
+
p.vy = p.y - prevY;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'electricity': {
|
|
567
|
+
if (p.startX !== undefined && p.endX !== undefined && p.startY !== undefined && p.endY !== undefined) {
|
|
568
|
+
if (!p.lastMidpointFrame || (p.maxLife - p.life) - (p.lastMidpointFrame ?? 0) >= 3) {
|
|
569
|
+
const segCount = 5 + Math.floor(Math.random() * 3);
|
|
570
|
+
p.midpoints = [];
|
|
571
|
+
for (let s = 1; s < segCount; s++) {
|
|
572
|
+
const t = s / segCount;
|
|
573
|
+
const mx = p.startX + (p.endX - p.startX) * t;
|
|
574
|
+
const my = p.startY + (p.endY - p.startY) * t;
|
|
575
|
+
const edx = p.endX - p.startX;
|
|
576
|
+
const edy = p.endY - p.startY;
|
|
577
|
+
const len = Math.sqrt(edx * edx + edy * edy) || 1;
|
|
578
|
+
const nx = -edy / len;
|
|
579
|
+
const ny = edx / len;
|
|
580
|
+
const offset = (Math.random() - 0.5) * 10;
|
|
581
|
+
p.midpoints.push({ x: mx + nx * offset, y: my + ny * offset });
|
|
582
|
+
}
|
|
583
|
+
p.lastMidpointFrame = p.maxLife - p.life;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case 'trail':
|
|
589
|
+
break;
|
|
590
|
+
case 'smoke': {
|
|
591
|
+
p.vy += p.gravity ?? -0.05;
|
|
592
|
+
p.vx += (Math.random() - 0.5) * 0.1;
|
|
593
|
+
p.x += p.vx;
|
|
594
|
+
p.y += p.vy;
|
|
595
|
+
p.size += 0.15;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case 'aura': {
|
|
599
|
+
// Attractor constraint for aura particles
|
|
600
|
+
const adx = attractorX - p.x;
|
|
601
|
+
const ady = attractorY - p.y;
|
|
602
|
+
const adist = Math.sqrt(adx * adx + ady * ady);
|
|
603
|
+
if (adist < 200 && adist > 0) {
|
|
604
|
+
p.x += adx * 0.02;
|
|
605
|
+
p.y += ady * 0.02;
|
|
606
|
+
}
|
|
607
|
+
// Derive velocity from position change (PBD)
|
|
608
|
+
p.vx = p.x - prevX;
|
|
609
|
+
p.vy = p.y - prevY;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
481
616
|
// ─── 4. PROCEDURAL SKY ──────────────────────────────────────────
|
|
482
617
|
// Stable star positions (seeded pseudorandom)
|
|
483
618
|
const STAR_POSITIONS = [];
|
|
@@ -1358,4 +1493,516 @@ export function renderPostProcessing(ctx, width, height, frame, options) {
|
|
|
1358
1493
|
}
|
|
1359
1494
|
ctx.restore();
|
|
1360
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Create an empty radiance grid (20x12 cells covering the 1280x720 canvas).
|
|
1498
|
+
*/
|
|
1499
|
+
export function createRadianceGrid() {
|
|
1500
|
+
return {
|
|
1501
|
+
cells: new Float32Array(20 * 12 * 3),
|
|
1502
|
+
width: 20,
|
|
1503
|
+
height: 12,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Update radiance grid by propagating light from all sources using inverse-square falloff.
|
|
1508
|
+
* Clears grid each frame before re-propagating.
|
|
1509
|
+
*/
|
|
1510
|
+
export function updateRadianceGrid(grid, lights) {
|
|
1511
|
+
// Clear
|
|
1512
|
+
grid.cells.fill(0);
|
|
1513
|
+
const cellW = 1280 / grid.width; // 64
|
|
1514
|
+
const cellH = 720 / grid.height; // 60
|
|
1515
|
+
for (const light of lights) {
|
|
1516
|
+
let intensity = light.intensity;
|
|
1517
|
+
if (light.flicker) {
|
|
1518
|
+
intensity *= 0.85 + Math.random() * 0.15;
|
|
1519
|
+
}
|
|
1520
|
+
const [lr, lg, lb] = hexToRgb(light.color);
|
|
1521
|
+
for (let cy = 0; cy < grid.height; cy++) {
|
|
1522
|
+
for (let cx = 0; cx < grid.width; cx++) {
|
|
1523
|
+
// Cell center in canvas space
|
|
1524
|
+
const cellCenterX = (cx + 0.5) * cellW;
|
|
1525
|
+
const cellCenterY = (cy + 0.5) * cellH;
|
|
1526
|
+
const dx = cellCenterX - light.x;
|
|
1527
|
+
const dy = cellCenterY - light.y;
|
|
1528
|
+
const distSq = dx * dx + dy * dy;
|
|
1529
|
+
// Inverse-square falloff with a softening factor
|
|
1530
|
+
const contribution = intensity / (1 + distSq * 0.001);
|
|
1531
|
+
// Skip negligible contributions
|
|
1532
|
+
if (contribution < 0.001)
|
|
1533
|
+
continue;
|
|
1534
|
+
const idx = (cy * grid.width + cx) * 3;
|
|
1535
|
+
grid.cells[idx] += (lr / 255) * contribution;
|
|
1536
|
+
grid.cells[idx + 1] += (lg / 255) * contribution;
|
|
1537
|
+
grid.cells[idx + 2] += (lb / 255) * contribution;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Render the radiance grid as an additive overlay.
|
|
1544
|
+
* Each grid cell is drawn as a colored rect at low opacity, creating ambient light propagation.
|
|
1545
|
+
*/
|
|
1546
|
+
export function renderRadianceOverlay(ctx, grid, width, height) {
|
|
1547
|
+
const cellW = width / grid.width;
|
|
1548
|
+
const cellH = height / grid.height;
|
|
1549
|
+
ctx.save();
|
|
1550
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1551
|
+
for (let cy = 0; cy < grid.height; cy++) {
|
|
1552
|
+
for (let cx = 0; cx < grid.width; cx++) {
|
|
1553
|
+
const idx = (cy * grid.width + cx) * 3;
|
|
1554
|
+
const r = grid.cells[idx];
|
|
1555
|
+
const g = grid.cells[idx + 1];
|
|
1556
|
+
const b = grid.cells[idx + 2];
|
|
1557
|
+
// Skip dark cells
|
|
1558
|
+
const magnitude = r + g + b;
|
|
1559
|
+
if (magnitude < 0.01)
|
|
1560
|
+
continue;
|
|
1561
|
+
// Map accumulated radiance to color, cap at 1.0
|
|
1562
|
+
const cr = Math.min(255, Math.round(Math.min(1, r) * 255));
|
|
1563
|
+
const cg = Math.min(255, Math.round(Math.min(1, g) * 255));
|
|
1564
|
+
const cb = Math.min(255, Math.round(Math.min(1, b) * 255));
|
|
1565
|
+
// Opacity scales with magnitude (0.08-0.15 range)
|
|
1566
|
+
const alpha = Math.min(0.15, 0.08 + magnitude * 0.02);
|
|
1567
|
+
ctx.fillStyle = `rgba(${cr},${cg},${cb},${alpha})`;
|
|
1568
|
+
ctx.fillRect(cx * cellW, cy * cellH, cellW, cellH);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1572
|
+
ctx.restore();
|
|
1573
|
+
}
|
|
1574
|
+
// ─── 9. SUBSURFACE SCATTERING APPROXIMATION ─────────────────────
|
|
1575
|
+
/**
|
|
1576
|
+
* Shift a hex color's hue toward warm (red-shifted) by a given amount.
|
|
1577
|
+
*/
|
|
1578
|
+
function warmShiftColor(hex, shiftDeg = 20) {
|
|
1579
|
+
const [r, g, b] = hexToRgb(hex);
|
|
1580
|
+
// Convert to HSL
|
|
1581
|
+
const rf = r / 255, gf = g / 255, bf = b / 255;
|
|
1582
|
+
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
1583
|
+
let h = 0, s = 0;
|
|
1584
|
+
const l = (max + min) / 2;
|
|
1585
|
+
if (max !== min) {
|
|
1586
|
+
const d = max - min;
|
|
1587
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1588
|
+
if (max === rf)
|
|
1589
|
+
h = ((gf - bf) / d + (gf < bf ? 6 : 0)) * 60;
|
|
1590
|
+
else if (max === gf)
|
|
1591
|
+
h = ((bf - rf) / d + 2) * 60;
|
|
1592
|
+
else
|
|
1593
|
+
h = ((rf - gf) / d + 4) * 60;
|
|
1594
|
+
}
|
|
1595
|
+
// Shift hue toward warm (lower hue = red/orange)
|
|
1596
|
+
h = (h + shiftDeg) % 360;
|
|
1597
|
+
// HSL to RGB
|
|
1598
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
1599
|
+
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
1600
|
+
const m = l - c / 2;
|
|
1601
|
+
let r1 = 0, g1 = 0, b1 = 0;
|
|
1602
|
+
if (h < 60) {
|
|
1603
|
+
r1 = c;
|
|
1604
|
+
g1 = x;
|
|
1605
|
+
b1 = 0;
|
|
1606
|
+
}
|
|
1607
|
+
else if (h < 120) {
|
|
1608
|
+
r1 = x;
|
|
1609
|
+
g1 = c;
|
|
1610
|
+
b1 = 0;
|
|
1611
|
+
}
|
|
1612
|
+
else if (h < 180) {
|
|
1613
|
+
r1 = 0;
|
|
1614
|
+
g1 = c;
|
|
1615
|
+
b1 = x;
|
|
1616
|
+
}
|
|
1617
|
+
else if (h < 240) {
|
|
1618
|
+
r1 = 0;
|
|
1619
|
+
g1 = x;
|
|
1620
|
+
b1 = c;
|
|
1621
|
+
}
|
|
1622
|
+
else if (h < 300) {
|
|
1623
|
+
r1 = x;
|
|
1624
|
+
g1 = 0;
|
|
1625
|
+
b1 = c;
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
r1 = c;
|
|
1629
|
+
g1 = 0;
|
|
1630
|
+
b1 = x;
|
|
1631
|
+
}
|
|
1632
|
+
return `rgb(${Math.round((r1 + m) * 255)},${Math.round((g1 + m) * 255)},${Math.round((b1 + m) * 255)})`;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Render subsurface scattering approximation on translucent robot panels.
|
|
1636
|
+
* Creates a soft warm glow "leaking through" panel edges using shadowBlur + screen compositing.
|
|
1637
|
+
*/
|
|
1638
|
+
export function renderSubsurfaceGlow(ctx, panels) {
|
|
1639
|
+
ctx.save();
|
|
1640
|
+
for (const panel of panels) {
|
|
1641
|
+
// Red-shift the color for subsurface warmth
|
|
1642
|
+
const sssColor = warmShiftColor(panel.color, 20);
|
|
1643
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1644
|
+
ctx.shadowColor = sssColor;
|
|
1645
|
+
ctx.shadowBlur = 8 + panel.intensity * 4;
|
|
1646
|
+
ctx.strokeStyle = sssColor;
|
|
1647
|
+
ctx.lineWidth = 2;
|
|
1648
|
+
ctx.globalAlpha = 0.4 * panel.intensity;
|
|
1649
|
+
// Draw just the border (not filled) for edge glow effect
|
|
1650
|
+
ctx.strokeRect(panel.x, panel.y, panel.width, panel.height);
|
|
1651
|
+
}
|
|
1652
|
+
// Reset
|
|
1653
|
+
ctx.shadowBlur = 0;
|
|
1654
|
+
ctx.shadowColor = 'transparent';
|
|
1655
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1656
|
+
ctx.globalAlpha = 1;
|
|
1657
|
+
ctx.restore();
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Build SSS panel definitions for the kbot character.
|
|
1661
|
+
*/
|
|
1662
|
+
export function buildSubsurfacePanels(robotX, robotY, scale, moodColor) {
|
|
1663
|
+
const panels = [];
|
|
1664
|
+
// Chest display panel
|
|
1665
|
+
panels.push({
|
|
1666
|
+
x: robotX + 12 * scale,
|
|
1667
|
+
y: robotY + 20 * scale,
|
|
1668
|
+
width: 10 * scale,
|
|
1669
|
+
height: 8 * scale,
|
|
1670
|
+
color: moodColor,
|
|
1671
|
+
intensity: 0.7,
|
|
1672
|
+
});
|
|
1673
|
+
// Left eye socket
|
|
1674
|
+
panels.push({
|
|
1675
|
+
x: robotX + 12 * scale,
|
|
1676
|
+
y: robotY + 8 * scale,
|
|
1677
|
+
width: 4 * scale,
|
|
1678
|
+
height: 3 * scale,
|
|
1679
|
+
color: moodColor,
|
|
1680
|
+
intensity: 0.5,
|
|
1681
|
+
});
|
|
1682
|
+
// Right eye socket
|
|
1683
|
+
panels.push({
|
|
1684
|
+
x: robotX + 18 * scale,
|
|
1685
|
+
y: robotY + 8 * scale,
|
|
1686
|
+
width: 4 * scale,
|
|
1687
|
+
height: 3 * scale,
|
|
1688
|
+
color: moodColor,
|
|
1689
|
+
intensity: 0.5,
|
|
1690
|
+
});
|
|
1691
|
+
// Antenna ball
|
|
1692
|
+
panels.push({
|
|
1693
|
+
x: robotX + 14 * scale,
|
|
1694
|
+
y: robotY - 4 * scale,
|
|
1695
|
+
width: 4 * scale,
|
|
1696
|
+
height: 4 * scale,
|
|
1697
|
+
color: moodColor,
|
|
1698
|
+
intensity: 0.8,
|
|
1699
|
+
});
|
|
1700
|
+
// Jet boot thrusters (left)
|
|
1701
|
+
panels.push({
|
|
1702
|
+
x: robotX + 10 * scale,
|
|
1703
|
+
y: robotY + 46 * scale,
|
|
1704
|
+
width: 5 * scale,
|
|
1705
|
+
height: 3 * scale,
|
|
1706
|
+
color: '#e8820c',
|
|
1707
|
+
intensity: 0.6,
|
|
1708
|
+
});
|
|
1709
|
+
// Jet boot thrusters (right)
|
|
1710
|
+
panels.push({
|
|
1711
|
+
x: robotX + 19 * scale,
|
|
1712
|
+
y: robotY + 46 * scale,
|
|
1713
|
+
width: 5 * scale,
|
|
1714
|
+
height: 3 * scale,
|
|
1715
|
+
color: '#e8820c',
|
|
1716
|
+
intensity: 0.6,
|
|
1717
|
+
});
|
|
1718
|
+
return panels;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Create an empty frame cache for importance-sampled rendering.
|
|
1722
|
+
*/
|
|
1723
|
+
export function createFrameCache() {
|
|
1724
|
+
return {
|
|
1725
|
+
backgroundLayer: null,
|
|
1726
|
+
bodyLayer: null,
|
|
1727
|
+
lastBackgroundFrame: -999,
|
|
1728
|
+
lastBodyFrame: -999,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Determine if a layer should be re-rendered this frame.
|
|
1733
|
+
* - Background: every 4th frame (cached)
|
|
1734
|
+
* - Body: every 2nd frame when idle (cached), always when moving
|
|
1735
|
+
* - Effects: every frame
|
|
1736
|
+
*/
|
|
1737
|
+
export function shouldRenderLayer(cache, layer, currentFrame, isMoving = false, moodChanged = false, worldChanged = false) {
|
|
1738
|
+
// Invalidate cache on mood/world changes
|
|
1739
|
+
if (moodChanged || worldChanged)
|
|
1740
|
+
return true;
|
|
1741
|
+
switch (layer) {
|
|
1742
|
+
case 'background':
|
|
1743
|
+
if (!cache.backgroundLayer)
|
|
1744
|
+
return true;
|
|
1745
|
+
return (currentFrame - cache.lastBackgroundFrame) >= 4;
|
|
1746
|
+
case 'body':
|
|
1747
|
+
if (!cache.bodyLayer)
|
|
1748
|
+
return true;
|
|
1749
|
+
if (isMoving)
|
|
1750
|
+
return true;
|
|
1751
|
+
return (currentFrame - cache.lastBodyFrame) >= 2;
|
|
1752
|
+
case 'effects':
|
|
1753
|
+
return true; // always render effects
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Cache a rendered layer as ImageData.
|
|
1758
|
+
*/
|
|
1759
|
+
export function cacheLayer(cache, ctx, layer, x, y, w, h, currentFrame) {
|
|
1760
|
+
if (layer === 'background') {
|
|
1761
|
+
cache.backgroundLayer = ctx.getImageData(x, y, w, h);
|
|
1762
|
+
cache.lastBackgroundFrame = currentFrame;
|
|
1763
|
+
}
|
|
1764
|
+
else {
|
|
1765
|
+
cache.bodyLayer = ctx.getImageData(x, y, w, h);
|
|
1766
|
+
cache.lastBodyFrame = currentFrame;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Draw a cached layer onto the canvas.
|
|
1771
|
+
*/
|
|
1772
|
+
export function drawCachedLayer(ctx, cache, layer) {
|
|
1773
|
+
if (layer === 'background' && cache.backgroundLayer) {
|
|
1774
|
+
ctx.putImageData(cache.backgroundLayer, 0, 0);
|
|
1775
|
+
}
|
|
1776
|
+
else if (layer === 'body' && cache.bodyLayer) {
|
|
1777
|
+
ctx.putImageData(cache.bodyLayer, 0, 0);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
// ─── 11. VOLUMETRIC FOG ─────────────────────────────────────────
|
|
1781
|
+
/**
|
|
1782
|
+
* Get fog density for a given biome.
|
|
1783
|
+
*/
|
|
1784
|
+
function getFogDensityForBiome(biome) {
|
|
1785
|
+
switch (biome) {
|
|
1786
|
+
case 'ocean': return 0.4;
|
|
1787
|
+
case 'lava': return 0.3;
|
|
1788
|
+
case 'grass': return 0.1;
|
|
1789
|
+
case 'space': return 0.0;
|
|
1790
|
+
case 'city': return 0.2;
|
|
1791
|
+
default: return 0.15;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Render volumetric fog with drifting horizontal bands.
|
|
1796
|
+
* Fog is thinner near light sources and varies by biome.
|
|
1797
|
+
*/
|
|
1798
|
+
export function renderVolumetricFog(ctx, width, height, frame, fogDensity, fogColor, lightSources) {
|
|
1799
|
+
if (fogDensity <= 0.01)
|
|
1800
|
+
return;
|
|
1801
|
+
const [fr, fg, fb] = hexToRgb(fogColor);
|
|
1802
|
+
const bandCount = 5;
|
|
1803
|
+
const bandYBase = [200, 300, 380, 430, 460]; // vertical positions
|
|
1804
|
+
const bandHeight = [60, 80, 70, 50, 40];
|
|
1805
|
+
ctx.save();
|
|
1806
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1807
|
+
const drift = Math.sin(frame * 0.02 + i * 1.7) * 20;
|
|
1808
|
+
const bandY = bandYBase[i];
|
|
1809
|
+
const bh = bandHeight[i];
|
|
1810
|
+
// Create horizontal gradient for this band (denser in middle)
|
|
1811
|
+
const grad = ctx.createLinearGradient(0, bandY, 0, bandY + bh);
|
|
1812
|
+
// Calculate light reduction at this band's center
|
|
1813
|
+
let lightReduction = 0;
|
|
1814
|
+
const bandCenterY = bandY + bh / 2;
|
|
1815
|
+
for (const light of lightSources) {
|
|
1816
|
+
const dx = (width / 2) - light.x;
|
|
1817
|
+
const dy = bandCenterY - light.y;
|
|
1818
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1819
|
+
if (dist < 200) {
|
|
1820
|
+
lightReduction += light.intensity * (1 - dist / 200) * 0.5;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const effectiveDensity = Math.max(0, fogDensity - lightReduction);
|
|
1824
|
+
if (effectiveDensity < 0.01)
|
|
1825
|
+
continue;
|
|
1826
|
+
const alpha = effectiveDensity * 0.15;
|
|
1827
|
+
grad.addColorStop(0, `rgba(${fr},${fg},${fb},0)`);
|
|
1828
|
+
grad.addColorStop(0.3, `rgba(${fr},${fg},${fb},${alpha})`);
|
|
1829
|
+
grad.addColorStop(0.5, `rgba(${fr},${fg},${fb},${alpha * 1.2})`);
|
|
1830
|
+
grad.addColorStop(0.7, `rgba(${fr},${fg},${fb},${alpha})`);
|
|
1831
|
+
grad.addColorStop(1, `rgba(${fr},${fg},${fb},0)`);
|
|
1832
|
+
ctx.fillStyle = grad;
|
|
1833
|
+
ctx.fillRect(drift, bandY, width - drift, bh);
|
|
1834
|
+
// Wrap-around for drift
|
|
1835
|
+
if (drift > 0) {
|
|
1836
|
+
ctx.fillRect(0, bandY, drift, bh);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
ctx.restore();
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Get fog parameters for the current world state.
|
|
1843
|
+
*/
|
|
1844
|
+
export function getFogParams(biome, timeOfDay) {
|
|
1845
|
+
const baseDensity = getFogDensityForBiome(biome);
|
|
1846
|
+
// Increase fog at night and dawn
|
|
1847
|
+
let timeMod = 1.0;
|
|
1848
|
+
if (timeOfDay === 'night')
|
|
1849
|
+
timeMod = 1.3;
|
|
1850
|
+
else if (timeOfDay === 'dawn')
|
|
1851
|
+
timeMod = 1.2;
|
|
1852
|
+
else if (timeOfDay === 'day')
|
|
1853
|
+
timeMod = 0.7;
|
|
1854
|
+
// Fog color varies by biome
|
|
1855
|
+
let fogColor = '#8899aa'; // default grey-blue
|
|
1856
|
+
if (biome === 'lava')
|
|
1857
|
+
fogColor = '#332211'; // smoky brown
|
|
1858
|
+
else if (biome === 'ocean')
|
|
1859
|
+
fogColor = '#667788'; // sea mist
|
|
1860
|
+
else if (biome === 'city')
|
|
1861
|
+
fogColor = '#556677'; // smog
|
|
1862
|
+
else if (biome === 'space')
|
|
1863
|
+
fogColor = '#111122';
|
|
1864
|
+
return {
|
|
1865
|
+
density: Math.min(1, baseDensity * timeMod),
|
|
1866
|
+
color: fogColor,
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
// ─── 12. PALETTE CYCLING (NEURAL TEXTURE COMPRESSION) ───────────
|
|
1870
|
+
/**
|
|
1871
|
+
* Convert hex color to HSL.
|
|
1872
|
+
*/
|
|
1873
|
+
function hexToHsl(hex) {
|
|
1874
|
+
const [r, g, b] = hexToRgb(hex);
|
|
1875
|
+
const rf = r / 255, gf = g / 255, bf = b / 255;
|
|
1876
|
+
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
1877
|
+
let h = 0, s = 0;
|
|
1878
|
+
const l = (max + min) / 2;
|
|
1879
|
+
if (max !== min) {
|
|
1880
|
+
const d = max - min;
|
|
1881
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1882
|
+
if (max === rf)
|
|
1883
|
+
h = ((gf - bf) / d + (gf < bf ? 6 : 0)) * 60;
|
|
1884
|
+
else if (max === gf)
|
|
1885
|
+
h = ((bf - rf) / d + 2) * 60;
|
|
1886
|
+
else
|
|
1887
|
+
h = ((rf - gf) / d + 4) * 60;
|
|
1888
|
+
}
|
|
1889
|
+
return [h, s * 100, l * 100];
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Convert HSL to hex color string.
|
|
1893
|
+
*/
|
|
1894
|
+
function hslToHex(h, s, l) {
|
|
1895
|
+
s /= 100;
|
|
1896
|
+
l /= 100;
|
|
1897
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
1898
|
+
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
1899
|
+
const m = l - c / 2;
|
|
1900
|
+
let r = 0, g = 0, b = 0;
|
|
1901
|
+
if (h < 60) {
|
|
1902
|
+
r = c;
|
|
1903
|
+
g = x;
|
|
1904
|
+
b = 0;
|
|
1905
|
+
}
|
|
1906
|
+
else if (h < 120) {
|
|
1907
|
+
r = x;
|
|
1908
|
+
g = c;
|
|
1909
|
+
b = 0;
|
|
1910
|
+
}
|
|
1911
|
+
else if (h < 180) {
|
|
1912
|
+
r = 0;
|
|
1913
|
+
g = c;
|
|
1914
|
+
b = x;
|
|
1915
|
+
}
|
|
1916
|
+
else if (h < 240) {
|
|
1917
|
+
r = 0;
|
|
1918
|
+
g = x;
|
|
1919
|
+
b = c;
|
|
1920
|
+
}
|
|
1921
|
+
else if (h < 300) {
|
|
1922
|
+
r = x;
|
|
1923
|
+
g = 0;
|
|
1924
|
+
b = c;
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
r = c;
|
|
1928
|
+
g = 0;
|
|
1929
|
+
b = x;
|
|
1930
|
+
}
|
|
1931
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, '0');
|
|
1932
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Cycle palette colors subtly based on frame, mood, and time of day.
|
|
1936
|
+
* Creates a living, shimmering effect using HSL-based color shifting.
|
|
1937
|
+
*/
|
|
1938
|
+
export function cyclePalette(basePalette, frame, mood, timeOfDay) {
|
|
1939
|
+
const cycled = {};
|
|
1940
|
+
let colorIndex = 0;
|
|
1941
|
+
for (const [key, hex] of Object.entries(basePalette)) {
|
|
1942
|
+
const [h, s, l] = hexToHsl(hex);
|
|
1943
|
+
// Subtle hue shimmer
|
|
1944
|
+
let newH = h + Math.sin(frame * 0.01 + colorIndex * 0.5) * 5;
|
|
1945
|
+
if (newH < 0)
|
|
1946
|
+
newH += 360;
|
|
1947
|
+
if (newH >= 360)
|
|
1948
|
+
newH -= 360;
|
|
1949
|
+
// Saturation boost for excited/dancing moods
|
|
1950
|
+
let newS = s;
|
|
1951
|
+
if (mood === 'excited' || mood === 'dancing') {
|
|
1952
|
+
newS = Math.min(100, s + 10);
|
|
1953
|
+
}
|
|
1954
|
+
// Lightness modulation by time of day
|
|
1955
|
+
let newL = l;
|
|
1956
|
+
if (timeOfDay === 'night')
|
|
1957
|
+
newL = Math.max(0, l - 10);
|
|
1958
|
+
else if (timeOfDay === 'day')
|
|
1959
|
+
newL = Math.min(100, l + 5);
|
|
1960
|
+
else if (timeOfDay === 'sunset') {
|
|
1961
|
+
// Warm hue shift for sunset
|
|
1962
|
+
newH = (newH + 15) % 360;
|
|
1963
|
+
}
|
|
1964
|
+
cycled[key] = hslToHex(newH, Math.max(0, Math.min(100, newS)), Math.max(0, Math.min(100, newL)));
|
|
1965
|
+
colorIndex++;
|
|
1966
|
+
}
|
|
1967
|
+
return cycled;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Compute animation parameters based on stream context.
|
|
1971
|
+
* Higher engagement = faster, more energetic animations.
|
|
1972
|
+
*/
|
|
1973
|
+
export function computeAnimationParams(chatRate, // messages per minute
|
|
1974
|
+
viewerCount, // estimated viewers
|
|
1975
|
+
mood, timeOfDay, streamDuration) {
|
|
1976
|
+
// Base energy from chat rate (0-10 msgs/min maps to 0.2-0.8)
|
|
1977
|
+
let energy = Math.min(0.8, 0.2 + chatRate * 0.06);
|
|
1978
|
+
// Viewer count boost (subtle)
|
|
1979
|
+
energy += Math.min(0.1, viewerCount * 0.005);
|
|
1980
|
+
// Mood modifiers
|
|
1981
|
+
if (mood === 'excited' || mood === 'dancing')
|
|
1982
|
+
energy = Math.min(1, energy + 0.2);
|
|
1983
|
+
else if (mood === 'dreaming')
|
|
1984
|
+
energy = 0.1;
|
|
1985
|
+
else if (mood === 'thinking')
|
|
1986
|
+
energy = Math.max(0.3, energy * 0.7);
|
|
1987
|
+
else if (mood === 'error')
|
|
1988
|
+
energy = Math.min(1, energy + 0.15);
|
|
1989
|
+
// Time of day
|
|
1990
|
+
if (timeOfDay === 'night')
|
|
1991
|
+
energy *= 0.75;
|
|
1992
|
+
else if (timeOfDay === 'dawn')
|
|
1993
|
+
energy *= 0.85;
|
|
1994
|
+
// Stream fatigue — slight decrease over time (max 20% reduction after 3 hours)
|
|
1995
|
+
const fatigueFactor = Math.max(0.8, 1 - (streamDuration / 180) * 0.2);
|
|
1996
|
+
energy *= fatigueFactor;
|
|
1997
|
+
// Clamp
|
|
1998
|
+
energy = Math.max(0.05, Math.min(1, energy));
|
|
1999
|
+
return {
|
|
2000
|
+
blinkRate: 0.1 + energy * 0.3,
|
|
2001
|
+
wobbleFreq: 0.02 + energy * 0.08,
|
|
2002
|
+
wobbleAmp: 1 + energy * 4,
|
|
2003
|
+
glowPulseSpeed: 0.05 + energy * 0.15,
|
|
2004
|
+
breathSpeed: 0.03 + energy * 0.07,
|
|
2005
|
+
energyLevel: energy,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
1361
2008
|
//# sourceMappingURL=render-engine.js.map
|