Optimize Brand Accent Component By Caching Dark Mode Ellipse
This commit is contained in:
1 parent
ed7ecb2994
commit
94ca7defb2
1 file changed
+93
-37
@@ -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++) {
|
||||
|
||||
Reference in new issue
Block a user