signature: restore lean dots, reduced-motion redraw, above-fold reveal and map focus rings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -209,4 +209,73 @@ describe('DotBackground', () => {
|
|||||||
|
|
||||||
expect(raf).toHaveBeenCalled();
|
expect(raf).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('starts with a bounded initial dot count on a large desktop viewport', async () => {
|
||||||
|
const context = mockContext();
|
||||||
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context);
|
||||||
|
const widthStub = vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(2560);
|
||||||
|
const heightStub = vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(1440);
|
||||||
|
const scheduled: FrameRequestCallback[] = [];
|
||||||
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||||
|
scheduled.push(callback);
|
||||||
|
return scheduled.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fixture = await createFixture();
|
||||||
|
const pending = scheduled.splice(0);
|
||||||
|
for (const callback of pending) {
|
||||||
|
callback(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcCalls = vi.mocked(context.arc).mock.calls.length;
|
||||||
|
expect(arcCalls).toBeGreaterThanOrEqual(6);
|
||||||
|
expect(arcCalls).toBeLessThanOrEqual(24);
|
||||||
|
fixture.destroy();
|
||||||
|
} finally {
|
||||||
|
widthStub.mockRestore();
|
||||||
|
heightStub.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redraws the static frame after resize under reduced motion without starting a loop', async () => {
|
||||||
|
mockMatchMedia((query) => query.includes('prefers-reduced-motion'));
|
||||||
|
const context = mockContext();
|
||||||
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context);
|
||||||
|
const scheduled: FrameRequestCallback[] = [];
|
||||||
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||||
|
scheduled.push(callback);
|
||||||
|
return scheduled.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DotBackground],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(DotBackground);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
TestBed.inject(ApplicationRef).tick();
|
||||||
|
|
||||||
|
const pending = scheduled.splice(0);
|
||||||
|
for (const callback of pending) {
|
||||||
|
callback(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(context.clearRect).mockClear();
|
||||||
|
vi.mocked(context.arc).mockClear();
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
|
||||||
|
const resizeFrames = scheduled.splice(0);
|
||||||
|
for (const callback of resizeFrames) {
|
||||||
|
callback(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(context.clearRect).toHaveBeenCalled();
|
||||||
|
expect(context.arc).toHaveBeenCalled();
|
||||||
|
expect(scheduled).toEqual([]);
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export class DotBackground {
|
|||||||
private tornDown = false;
|
private tornDown = false;
|
||||||
private readonly teardowns: Array<() => void> = [];
|
private readonly teardowns: Array<() => void> = [];
|
||||||
|
|
||||||
private readonly MIN_DOT_COUNT = 8;
|
private readonly INITIAL_DOT_COUNT_MIN = 6;
|
||||||
|
private readonly INITIAL_DOT_COUNT_MAX = 24;
|
||||||
|
private readonly INITIAL_DOT_COUNT_MAX_COARSE = 12;
|
||||||
|
private readonly INITIAL_DOT_AREA_DIVISOR = 130_000;
|
||||||
private readonly MAX_DOT_COUNT = 100;
|
private readonly MAX_DOT_COUNT = 100;
|
||||||
private readonly MAX_DOT_COUNT_MOBILE = 40;
|
private readonly MAX_DOT_COUNT_MOBILE = 40;
|
||||||
private readonly COLORS = ['#6366f1', '#8b5cf6', '#a855f7', '#3b82f6'];
|
private readonly COLORS = ['#6366f1', '#8b5cf6', '#a855f7', '#3b82f6'];
|
||||||
@@ -213,6 +216,10 @@ export class DotBackground {
|
|||||||
dot.x = Math.max(dot.radius, Math.min(width - dot.radius, dot.x));
|
dot.x = Math.max(dot.radius, Math.min(width - dot.radius, dot.x));
|
||||||
dot.y = Math.max(dot.radius, Math.min(height - dot.radius, dot.y));
|
dot.y = Math.max(dot.radius, Math.min(height - dot.radius, dot.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.reducedMotion && this.ctx) {
|
||||||
|
this.drawFrame();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,10 +246,12 @@ export class DotBackground {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private targetDotCount(width: number, height: number): number {
|
private targetDotCount(width: number, height: number): number {
|
||||||
const maxCount = this.coarsePointer ? this.MAX_DOT_COUNT_MOBILE : this.MAX_DOT_COUNT;
|
const maxInitial = this.coarsePointer
|
||||||
|
? this.INITIAL_DOT_COUNT_MAX_COARSE
|
||||||
|
: this.INITIAL_DOT_COUNT_MAX;
|
||||||
const area = Math.max(0, width) * Math.max(0, height);
|
const area = Math.max(0, width) * Math.max(0, height);
|
||||||
const fromArea = Math.round(area / 20_000);
|
const fromArea = Math.round(area / this.INITIAL_DOT_AREA_DIVISOR);
|
||||||
return Math.min(maxCount, Math.max(this.MIN_DOT_COUNT, fromArea));
|
return Math.min(maxInitial, Math.max(this.INITIAL_DOT_COUNT_MIN, fromArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
private spawnDot(): Dot {
|
private spawnDot(): Dot {
|
||||||
|
|||||||
@@ -124,10 +124,16 @@ describe('RevealDirective', () => {
|
|||||||
const element = hostElement(fixture);
|
const element = hostElement(fixture);
|
||||||
const observer = MockIntersectionObserver.instances[0];
|
const observer = MockIntersectionObserver.instances[0];
|
||||||
|
|
||||||
expect(element.classList.contains('reveal-pending')).toBe(true);
|
|
||||||
expect(element.classList.contains('is-revealed')).toBe(false);
|
|
||||||
expect(observer).toBeTruthy();
|
expect(observer).toBeTruthy();
|
||||||
expect(observer.observe).toHaveBeenCalled();
|
expect(observer.observe).toHaveBeenCalled();
|
||||||
|
expect(element.classList.contains('reveal-pending')).toBe(false);
|
||||||
|
expect(element.classList.contains('is-revealed')).toBe(false);
|
||||||
|
|
||||||
|
observer.trigger(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(element.classList.contains('reveal-pending')).toBe(true);
|
||||||
|
expect(element.classList.contains('is-revealed')).toBe(false);
|
||||||
|
|
||||||
observer.trigger(true);
|
observer.trigger(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -137,6 +143,31 @@ describe('RevealDirective', () => {
|
|||||||
expect(observer.disconnect).toHaveBeenCalled();
|
expect(observer.disconnect).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reveals on a first intersecting callback without ever adding reveal-pending', async () => {
|
||||||
|
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
||||||
|
|
||||||
|
const fixture = await createHost();
|
||||||
|
const element = hostElement(fixture);
|
||||||
|
const observer = MockIntersectionObserver.instances[0];
|
||||||
|
const addedTokens: string[] = [];
|
||||||
|
const add = element.classList.add.bind(element.classList);
|
||||||
|
vi.spyOn(element.classList, 'add').mockImplementation((...tokens: string[]) => {
|
||||||
|
addedTokens.push(...tokens);
|
||||||
|
add(...tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(observer).toBeTruthy();
|
||||||
|
expect(element.classList.contains('reveal-pending')).toBe(false);
|
||||||
|
|
||||||
|
observer.trigger(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(addedTokens).not.toContain('reveal-pending');
|
||||||
|
expect(element.classList.contains('reveal-pending')).toBe(false);
|
||||||
|
expect(element.classList.contains('is-revealed')).toBe(true);
|
||||||
|
expect(observer.disconnect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('disconnects when destroyed before intersection', async () => {
|
it('disconnects when destroyed before intersection', async () => {
|
||||||
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ export class RevealDirective {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.host.nativeElement.classList.add('reveal-pending');
|
|
||||||
this.observer = new view.IntersectionObserver(
|
this.observer = new view.IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries.some((entry) => entry.isIntersecting)) {
|
if (entries.some((entry) => entry.isIntersecting)) {
|
||||||
this.host.nativeElement.classList.remove('reveal-pending');
|
this.host.nativeElement.classList.remove('reveal-pending');
|
||||||
this.host.nativeElement.classList.add('is-revealed');
|
this.host.nativeElement.classList.add('is-revealed');
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.host.nativeElement.classList.add('reveal-pending');
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rootMargin: '0px 0px -8% 0px',
|
rootMargin: '0px 0px -8% 0px',
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
|
|
||||||
.systems-map-node {
|
.systems-map-node {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.systems-map-node circle,
|
.systems-map-node circle,
|
||||||
@@ -126,6 +125,18 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.systems-map-node:focus-visible,
|
||||||
|
.systems-map-cards a:focus-visible {
|
||||||
|
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||||||
|
outline-offset: var(--focus-ring-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.systems-map-node:focus-visible circle,
|
||||||
|
.systems-map-node:focus-visible rect {
|
||||||
|
stroke: var(--focus-ring-color);
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.systems-map-card-label {
|
.systems-map-card-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user