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 bgGradient!: CanvasGradient;
|
||||||
private gridGradient!: CanvasGradient;
|
private gridGradient!: CanvasGradient;
|
||||||
|
|
||||||
|
private offscreenCanvas!: OffscreenCanvas;
|
||||||
|
private offscreenCtx!: OffscreenCanvasRenderingContext2D;
|
||||||
|
|
||||||
private resizeObserver?: ResizeObserver;
|
private resizeObserver?: ResizeObserver;
|
||||||
private intersectionObserver?: IntersectionObserver;
|
private intersectionObserver?: IntersectionObserver;
|
||||||
|
|
||||||
|
private rendererScale!: number;
|
||||||
|
|
||||||
private animation: {
|
private animation: {
|
||||||
id?: number;
|
id?: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
@@ -48,19 +53,30 @@ export class BrandAccent implements OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(private ngZone: NgZone) {
|
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(() => {
|
effect(() => {
|
||||||
// Trigger cache refresh when dark mode changes
|
// Trigger cache refresh when dark mode changes
|
||||||
this.settingsService.darkMode();
|
const isDark = this.settingsService.darkMode();
|
||||||
if (this.ctx) {
|
if (this.ctx) {
|
||||||
this.cacheGradients();
|
this.cacheGradients(isDark);
|
||||||
|
this.cacheBackground(isDark);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
this.ctx = this.canvasRef.nativeElement.getContext('2d')!;
|
this.ctx = this.canvasRef.nativeElement.getContext('2d', { colorSpace: 'srgb' })!;
|
||||||
|
|
||||||
this.ngZone.runOutsideAngular(() => {
|
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.resizeObserver.observe(this.canvasRef.nativeElement);
|
||||||
|
|
||||||
this.intersectionObserver = new IntersectionObserver(
|
this.intersectionObserver = new IntersectionObserver(
|
||||||
@@ -71,11 +87,15 @@ export class BrandAccent implements OnDestroy {
|
|||||||
cancelAnimationFrame(this.animation.id);
|
cancelAnimationFrame(this.animation.id);
|
||||||
this.animation.id = undefined;
|
this.animation.id = undefined;
|
||||||
}
|
}
|
||||||
}, { rootMargin: '200px' }
|
},
|
||||||
|
{ rootMargin: '200px' },
|
||||||
);
|
);
|
||||||
this.intersectionObserver.observe(this.canvasRef.nativeElement);
|
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.animation.startTime = performance.now();
|
||||||
|
|
||||||
this.draw();
|
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 canvas = this.canvasRef.nativeElement;
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
canvas.width = rect.width * dpr;
|
this.rendererScale = dpr;
|
||||||
canvas.height = rect.height * 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.ctx.scale(dpr, dpr);
|
||||||
this.updateAnimationInfo();
|
this.updateAnimationInfo();
|
||||||
@@ -109,13 +135,16 @@ export class BrandAccent implements OnDestroy {
|
|||||||
Math.trunc(this.canvasNativeSize[1] / this.animation.gridSquareSize) + 3,
|
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 w = this.canvasNativeSize[0];
|
||||||
let h = this.canvasNativeSize[1];
|
let h = this.canvasNativeSize[1];
|
||||||
const isDark = this.settingsService.darkMode();
|
if (w === 0 || h === 0) return;
|
||||||
|
|
||||||
if (!isDark) {
|
if (!isDark) {
|
||||||
this.bgGradient = this.ctx.createLinearGradient(0, 0, w, h);
|
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) => {
|
private animate = (currentTime: number) => {
|
||||||
this.animation.id = requestAnimationFrame(this.animate);
|
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]);
|
this.ctx.clearRect(0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
|
||||||
const isDark = this.settingsService.darkMode();
|
const isDark = this.settingsService.darkMode();
|
||||||
|
|
||||||
if (!isDark) {
|
if (this.offscreenCanvas) {
|
||||||
this.ctx.globalCompositeOperation = 'source-over';
|
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';
|
this.ctx.globalCompositeOperation = 'multiply';
|
||||||
} else {
|
} 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';
|
this.ctx.globalCompositeOperation = 'screen';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +260,7 @@ export class BrandAccent implements OnDestroy {
|
|||||||
const yOffset =
|
const yOffset =
|
||||||
(this.canvasNativeSize[1] - this.animation.gridDimension[1] * this.animation.gridSquareSize) /
|
(this.canvasNativeSize[1] - this.animation.gridDimension[1] * this.animation.gridSquareSize) /
|
||||||
2;
|
2;
|
||||||
|
|
||||||
const movingOffset = -(this.animation.gridSquareSize * this.animation.progress);
|
const movingOffset = -(this.animation.gridSquareSize * this.animation.progress);
|
||||||
|
|
||||||
for (let y = 0; y < this.animation.gridDimension[1]; y++) {
|
for (let y = 0; y < this.animation.gridDimension[1]; y++) {
|
||||||
|
|||||||
Reference in new issue
Block a user