Optimize Brand Accent Component By Caching Dark Mode Ellipse

This commit is contained in:
copecone committed 2026-04-09 16:10:45 +09:00
1 parent ed7ecb2994
commit 94ca7defb2
1 file changed
+93 -37
+93 -37
View File
@@ -27,9 +27,14 @@ export class BrandAccent implements OnDestroy {
private bgGradient!: CanvasGradient;
private gridGradient!: CanvasGradient;
private offscreenCanvas!: OffscreenCanvas;
private offscreenCtx!: OffscreenCanvasRenderingContext2D;
private resizeObserver?: ResizeObserver;
private intersectionObserver?: IntersectionObserver;
private rendererScale!: number;
private animation: {
id?: number;
startTime: number;
@@ -48,19 +53,30 @@ export class BrandAccent implements OnDestroy {
};
constructor(private ngZone: NgZone) {
if (typeof document !== 'undefined') {
this.offscreenCanvas = new OffscreenCanvas(0, 0);
this.offscreenCtx = this.offscreenCanvas.getContext('2d', { alpha: false, colorSpace: 'srgb' })!;
}
effect(() => {
// Trigger cache refresh when dark mode changes
this.settingsService.darkMode();
const isDark = this.settingsService.darkMode();
if (this.ctx) {
this.cacheGradients();
this.cacheGradients(isDark);
this.cacheBackground(isDark);
}
});
afterNextRender(() => {
this.ctx = this.canvasRef.nativeElement.getContext('2d')!;
this.ctx = this.canvasRef.nativeElement.getContext('2d', { colorSpace: 'srgb' })!;
this.ngZone.runOutsideAngular(() => {
this.resizeObserver = new ResizeObserver(() => this.resize());
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { inlineSize: width, blockSize: height } = entry.borderBoxSize[0];
this.resize(width, height);
}
});
this.resizeObserver.observe(this.canvasRef.nativeElement);
this.intersectionObserver = new IntersectionObserver(
@@ -71,11 +87,15 @@ export class BrandAccent implements OnDestroy {
cancelAnimationFrame(this.animation.id);
this.animation.id = undefined;
}
}, { rootMargin: '200px' }
},
{ rootMargin: '200px' },
);
this.intersectionObserver.observe(this.canvasRef.nativeElement);
this.resize(); // Safe Initialization
// Safe Initialization
const rect = this.canvasRef.nativeElement.getBoundingClientRect();
this.resize(rect.width, rect.height);
this.animation.startTime = performance.now();
this.draw();
@@ -84,13 +104,19 @@ export class BrandAccent implements OnDestroy {
});
}
private resize() {
private resize(width: number, height: number) {
const canvas = this.canvasRef.nativeElement;
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
this.rendererScale = dpr;
canvas.width = width * dpr;
canvas.height = height * dpr;
if (this.offscreenCanvas) {
this.offscreenCanvas.width = width;
this.offscreenCanvas.height = height;
}
this.ctx.scale(dpr, dpr);
this.updateAnimationInfo();
@@ -109,13 +135,16 @@ export class BrandAccent implements OnDestroy {
Math.trunc(this.canvasNativeSize[1] / this.animation.gridSquareSize) + 3,
];
this.cacheGradients();
const isDark = this.settingsService.darkMode();
this.cacheGradients(isDark);
this.cacheBackground(isDark);
}
private cacheGradients() {
private cacheGradients(isDark: boolean) {
let w = this.canvasNativeSize[0];
let h = this.canvasNativeSize[1];
const isDark = this.settingsService.darkMode();
if (w === 0 || h === 0) return;
if (!isDark) {
this.bgGradient = this.ctx.createLinearGradient(0, 0, w, h);
@@ -135,6 +164,47 @@ export class BrandAccent implements OnDestroy {
}
}
private cacheBackground(isDark: boolean) {
if (!this.offscreenCtx) return;
const w = this.canvasNativeSize[0];
const h = this.canvasNativeSize[1];
if (w === 0 || h === 0) return;
this.offscreenCtx.clearRect(0, 0, w, h);
this.offscreenCtx.globalCompositeOperation = 'source-over';
if (!isDark) {
this.offscreenCtx.fillStyle = this.bgGradient;
this.offscreenCtx.fillRect(0, 0, w, h);
} else {
this.offscreenCtx.fillStyle = 'oklch(0.1776 0 0)';
this.offscreenCtx.fillRect(0, 0, w, h);
const ellipseDimension = [Math.max(100, w * 0.9421875), Math.max(200 / 3, h * (203 / 120))];
const ellipseX = w / 2;
const ellipseY = h * 0.5763888888 + ellipseDimension[1] / 2;
const blurScale =
Math.min(780, Math.min(ellipseDimension[0], ellipseDimension[1]) * 0.6) /
this.rendererScale;
this.offscreenCtx.filter = `blur(${blurScale}px)`;
this.offscreenCtx.beginPath();
this.offscreenCtx.ellipse(
ellipseX, ellipseY,
ellipseDimension[0] / 2, ellipseDimension[1] / 2,
0, 0, 2 * Math.PI,
);
this.offscreenCtx.fillStyle = 'oklch(0.2547 0.0913 29.02)';
this.offscreenCtx.fill();
this.offscreenCtx.filter = 'none';
}
}
private animate = (currentTime: number) => {
this.animation.id = requestAnimationFrame(this.animate);
@@ -162,33 +232,18 @@ export class BrandAccent implements OnDestroy {
this.ctx.clearRect(0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
const isDark = this.settingsService.darkMode();
if (!isDark) {
if (this.offscreenCanvas) {
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.fillStyle = this.bgGradient;
this.ctx.fillRect(0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(this.offscreenCanvas, 0, 0);
// this.ctx.drawImage(this.offscreenCanvas, 0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
}
if (!isDark) {
this.ctx.globalCompositeOperation = 'multiply';
} else {
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.fillStyle = 'oklch(0.1776 0 0)';
this.ctx.fillRect(0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
const ellipseDimension = [
Math.max(100, this.canvasNativeSize[0] * 0.9421875),
Math.max(200 / 3, this.canvasNativeSize[1] * (203 / 120)),
];
const ellipseX = this.canvasNativeSize[0] / 2;
const ellipseY = this.canvasNativeSize[1] * 0.5763888888 + ellipseDimension[1] / 2;
this.ctx.filter = `blur(${Math.min(780, Math.min(ellipseDimension[0], ellipseDimension[1]) * 0.6)}px)`;
this.ctx.beginPath();
this.ctx.ellipse(ellipseX, ellipseY, ellipseDimension[0] / 2, ellipseDimension[1] / 2, 0, 0, 2 * Math.PI);
this.ctx.fillStyle = 'oklch(0.2547 0.0913 29.02)';
this.ctx.fill();
this.ctx.filter = 'none';
this.ctx.globalCompositeOperation = 'screen';
}
@@ -205,6 +260,7 @@ export class BrandAccent implements OnDestroy {
const yOffset =
(this.canvasNativeSize[1] - this.animation.gridDimension[1] * this.animation.gridSquareSize) /
2;
const movingOffset = -(this.animation.gridSquareSize * this.animation.progress);
for (let y = 0; y < this.animation.gridDimension[1]; y++) {