Skip to content

Commit ac02f41

Browse files
authored
Merge pull request #5 from rubenvieira/claude/redesign-simulator-realistic-sEjrT
Fix desktop rendering: bloom self-draw, rivulet perf, condensation perf
2 parents 2ad3ed2 + 97c3377 commit ac02f41

1 file changed

Lines changed: 65 additions & 37 deletions

File tree

src/pages/Index.tsx

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,13 @@ const Index = () => {
389389

390390
// Bloom offscreen canvas (1/4 res)
391391
const bloomCanvasRef = useRef<HTMLCanvasElement | null>(null);
392+
const bloomWorkCanvasRef = useRef<HTMLCanvasElement | null>(null);
392393
const bloomFrameRef = useRef(0);
393394

395+
// Rivulet offscreen canvas (updated periodically)
396+
const rivuletCanvasRef = useRef<HTMLCanvasElement | null>(null);
397+
const rivuletLastUpdateRef = useRef(0);
398+
394399
// Settings ref for animation loop access
395400
const settingsRef = useRef(settings);
396401
settingsRef.current = settings;
@@ -713,44 +718,44 @@ const Index = () => {
713718
}
714719

715720
// === LAYER 2.5: RIVULET CHANNELS ===
716-
// Render subtle wet paths where drops have traveled
721+
// Render to offscreen canvas periodically (every 150ms) to avoid 32K+ draw calls per frame
717722
const rivMap = sim.rivuletMap;
718-
const rivData = rivMap.getData();
719-
const rivCols = rivMap.cols;
720-
const rivRows = rivMap.rows;
721-
const rivCell = rivMap.cellSize;
722-
ctx.save();
723-
for (let r = 0; r < rivRows; r++) {
724-
for (let c = 0; c < rivCols; c++) {
725-
const strength = rivData[r * rivCols + c];
726-
if (strength > 0.05) {
727-
ctx.fillStyle = `rgba(0, 5, 15, ${strength * 0.08})`;
728-
ctx.fillRect(c * rivCell, r * rivCell, rivCell, rivCell);
723+
if (timestamp - rivuletLastUpdateRef.current > 150) {
724+
rivuletLastUpdateRef.current = timestamp;
725+
if (!rivuletCanvasRef.current || rivuletCanvasRef.current.width !== width || rivuletCanvasRef.current.height !== height) {
726+
const rc = document.createElement('canvas');
727+
rc.width = width;
728+
rc.height = height;
729+
rivuletCanvasRef.current = rc;
730+
}
731+
const rCtx = rivuletCanvasRef.current.getContext('2d')!;
732+
rCtx.clearRect(0, 0, width, height);
733+
const rivData = rivMap.getData();
734+
const rivCols = rivMap.cols;
735+
const rivRows = rivMap.rows;
736+
const rivCell = rivMap.cellSize;
737+
for (let r = 0; r < rivRows; r++) {
738+
for (let c = 0; c < rivCols; c++) {
739+
const strength = rivData[r * rivCols + c];
740+
if (strength > 0.05) {
741+
rCtx.fillStyle = `rgba(0, 5, 15, ${strength * 0.08})`;
742+
rCtx.fillRect(c * rivCell, r * rivCell, rivCell, rivCell);
743+
}
729744
}
730745
}
731746
}
732-
ctx.restore();
747+
if (rivuletCanvasRef.current) {
748+
ctx.drawImage(rivuletCanvasRef.current, 0, 0);
749+
}
733750

734751
// === LAYER 2.6: CONDENSATION MICRO-DROPS ===
735752
if ((s.condensation ?? 60) > 0) {
736753
const now = timestamp;
737754
if (now - condensationLastUpdateRef.current > PERF.condensationUpdateInterval || !condensationCanvasRef.current) {
738755
condensationLastUpdateRef.current = now;
739756
const count = Math.floor(PERF.maxCondensation * ((s.condensation ?? 60) / 100));
740-
const drops = generateCondensation(width, height, count);
741-
742-
// Remove condensation near main droplets (absorption wake)
743-
const mainDroplets = sim.getDroplets();
744-
const filteredDrops = drops.filter(cd => {
745-
for (let d = 0; d < mainDroplets.length; d++) {
746-
const md = mainDroplets[d];
747-
const ddx = cd.x - md.x;
748-
const ddy = cd.y - md.y;
749-
if (ddx * ddx + ddy * ddy < (md.baseRadius * 3) * (md.baseRadius * 3)) return false;
750-
}
751-
return true;
752-
});
753-
condensationRef.current = filteredDrops;
757+
const microDrops = generateCondensation(width, height, count);
758+
condensationRef.current = microDrops;
754759

755760
// Render to offscreen canvas
756761
if (!condensationCanvasRef.current || condensationCanvasRef.current.width !== width) {
@@ -761,19 +766,30 @@ const Index = () => {
761766
}
762767
const cCtx = condensationCanvasRef.current.getContext('2d')!;
763768
cCtx.clearRect(0, 0, width, height);
764-
for (const cd of filteredDrops) {
769+
for (const cd of microDrops) {
765770
cCtx.beginPath();
766771
cCtx.arc(cd.x, cd.y, cd.radius, 0, Math.PI * 2);
767772
cCtx.fillStyle = `rgba(180, 200, 220, ${cd.opacity * 0.15})`;
768773
cCtx.fill();
769-
// Tiny specular dot
770774
if (cd.radius > 1) {
771775
cCtx.beginPath();
772776
cCtx.arc(cd.x - cd.radius * 0.3, cd.y - cd.radius * 0.3, Math.max(0.5, cd.radius * 0.25), 0, Math.PI * 2);
773777
cCtx.fillStyle = `rgba(255, 255, 255, ${cd.opacity * 0.4})`;
774778
cCtx.fill();
775779
}
776780
}
781+
// Clear condensation near main droplets using canvas compositing (fast)
782+
const mainDroplets = sim.getDroplets();
783+
cCtx.globalCompositeOperation = 'destination-out';
784+
for (let d = 0; d < mainDroplets.length; d++) {
785+
const md = mainDroplets[d];
786+
const clearR = md.baseRadius * 3;
787+
cCtx.beginPath();
788+
cCtx.arc(md.x, md.y, clearR, 0, Math.PI * 2);
789+
cCtx.fillStyle = 'rgba(0,0,0,1)';
790+
cCtx.fill();
791+
}
792+
cCtx.globalCompositeOperation = 'source-over';
777793
}
778794
if (condensationCanvasRef.current) {
779795
ctx.drawImage(condensationCanvasRef.current, 0, 0);
@@ -1433,22 +1449,31 @@ const Index = () => {
14331449
bc.height = bh;
14341450
bloomCanvasRef.current = bc;
14351451
}
1436-
const bCtx = bloomCanvasRef.current.getContext('2d')!;
1437-
bCtx.clearRect(0, 0, bw, bh);
1438-
// Draw bright specular positions as white dots
1452+
if (!bloomWorkCanvasRef.current || bloomWorkCanvasRef.current.width !== bw) {
1453+
const wc = document.createElement('canvas');
1454+
wc.width = bw;
1455+
wc.height = bh;
1456+
bloomWorkCanvasRef.current = wc;
1457+
}
1458+
const wCtx = bloomWorkCanvasRef.current.getContext('2d')!;
1459+
wCtx.clearRect(0, 0, bw, bh);
1460+
// Draw bright specular positions as white dots to work canvas
14391461
for (let i = 0; i < droplets.length; i++) {
14401462
const drop = droplets[i];
14411463
if (drop.baseRadius > 5 && drop.opacity > 0.5) {
14421464
const bx = (drop.x - drop.baseRadius * 0.3) / 4;
14431465
const by = (drop.y - drop.baseRadius * 0.33) / 4;
1444-
bCtx.beginPath();
1445-
bCtx.arc(bx, by, Math.max(1, drop.baseRadius * 0.06), 0, Math.PI * 2);
1446-
bCtx.fillStyle = 'rgba(255, 255, 255, 0.6)';
1447-
bCtx.fill();
1466+
wCtx.beginPath();
1467+
wCtx.arc(bx, by, Math.max(1, drop.baseRadius * 0.06), 0, Math.PI * 2);
1468+
wCtx.fillStyle = 'rgba(255, 255, 255, 0.6)';
1469+
wCtx.fill();
14481470
}
14491471
}
1472+
// Blur from work canvas to bloom canvas (never self-draw)
1473+
const bCtx = bloomCanvasRef.current.getContext('2d')!;
1474+
bCtx.clearRect(0, 0, bw, bh);
14501475
bCtx.filter = 'blur(8px)';
1451-
bCtx.drawImage(bloomCanvasRef.current, 0, 0);
1476+
bCtx.drawImage(bloomWorkCanvasRef.current, 0, 0);
14521477
bCtx.filter = 'none';
14531478
}
14541479
if (bloomCanvasRef.current) {
@@ -1603,6 +1628,9 @@ const Index = () => {
16031628
condensationCanvasRef.current = null;
16041629
condensationLastUpdateRef.current = 0;
16051630
bloomCanvasRef.current = null;
1631+
bloomWorkCanvasRef.current = null;
1632+
rivuletCanvasRef.current = null;
1633+
rivuletLastUpdateRef.current = 0;
16061634
simulationRef.current.populate(settings);
16071635
const canvas = canvasRef.current;
16081636
if (canvas) {

0 commit comments

Comments
 (0)