diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvasEffect.tsx b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvasEffect.tsx index f5ee2a9513..5cca25a397 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvasEffect.tsx +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvasEffect.tsx @@ -33,9 +33,6 @@ export const WelcomeHalftoneCanvasEffect = ({ const dotColor = canvasStyles .getPropertyValue('--welcome-dot-color') .trim(); - const dotHighlightColor = - canvasStyles.getPropertyValue('--welcome-dot-highlight').trim() || - dotColor; const readDevicePixelRatio = () => Math.min(window.devicePixelRatio || 1, 2); const readCanvasClientSize = () => ({ @@ -47,7 +44,6 @@ export const WelcomeHalftoneCanvasEffect = ({ const sharedControllerOptions = { canvas, dotColor, - dotHighlightColor, prefersReducedMotion, initialCanvasWidth: initialCanvasSize.width, initialCanvasHeight: initialCanvasSize.height, diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeOverlay.tsx b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeOverlay.tsx index 244e1bdfe1..dec8109d0f 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeOverlay.tsx +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeOverlay.tsx @@ -141,23 +141,6 @@ const StyledTitle = styled.div` } `; -const StyledTitleSurface = styled.div` - background: ${themeCssVariables.background.primary}; - border-radius: ${themeCssVariables.border.radius.pill}; - inset: 0; - position: absolute; - - .is-flying & { - animation: welcomeTitleSurfaceOut 0.24s ease-out forwards; - } - - @keyframes welcomeTitleSurfaceOut { - to { - opacity: 0; - } - } -`; - const StyledTitleBoldRun = styled.span` align-items: center; display: inline-flex; @@ -303,7 +286,6 @@ export const WelcomeOverlay = () => { className={titleClassName} style={titleStyle} > - {welcomeTitleWords.map((word, index) => ( ` align-items: center; - background: ${themeCssVariables.background.transparent.light}; - border-radius: ${({ sizeVariant }) => - sizeVariant === 'compact' - ? themeCssVariables.border.radius.sm - : themeCssVariables.border.radius.md}; display: inline-flex; gap: ${({ sizeVariant }) => sizeVariant === 'compact' diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/__tests__/buildWelcomeHalftoneParticles.test.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/__tests__/buildWelcomeHalftoneParticles.test.ts new file mode 100644 index 0000000000..c8803b3782 --- /dev/null +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/__tests__/buildWelcomeHalftoneParticles.test.ts @@ -0,0 +1,75 @@ +import { buildWelcomeHalftoneParticles } from '@/onboarding/components/WelcomeOverlay/buildWelcomeHalftoneParticles'; + +const CANVAS_WIDTH = 1280; +const CANVAS_HEIGHT = 832; +const VIEWBOX_WIDTH = 297.037; +const MINIMUM_STROKE_WIDTH = 0.6; +const VIEWBOX_TO_CANVAS_SCALE = + Math.max(CANVAS_WIDTH * 1.05, CANVAS_HEIGHT * 1.3) / VIEWBOX_WIDTH; + +describe('buildWelcomeHalftoneParticles', () => { + it('should elongate zero length dashes to the halftone aspect ratio', () => { + const { particles } = buildWelcomeHalftoneParticles( + [[148.5, 148.5, 119.5, 2]], + CANVAS_WIDTH, + CANVAS_HEIGHT, + ); + + expect(particles[0].dashLength).toBeCloseTo( + 2 * VIEWBOX_TO_CANVAS_SCALE * 1.452, + ); + expect(particles[0].strokeWidth).toBeCloseTo(2 * VIEWBOX_TO_CANVAS_SCALE); + }); + + it('should keep the dash length when the source dash is longer than the aspect ratio', () => { + const { particles } = buildWelcomeHalftoneParticles( + [[148.5, 158.5, 119.5, 2]], + CANVAS_WIDTH, + CANVAS_HEIGHT, + ); + + expect(particles[0].dashLength).toBeCloseTo(10 * VIEWBOX_TO_CANVAS_SCALE); + }); + + it('should keep the dash length proportional to the source stroke width when the minimum stroke width applies', () => { + const smallCanvasWidth = 600; + const smallCanvasHeight = 500; + const smallCanvasScale = + Math.max(smallCanvasWidth * 1.05, smallCanvasHeight * 1.3) / + VIEWBOX_WIDTH; + const fainterSourceStrokeWidth = 0.19; + const denserSourceStrokeWidth = 0.25; + + const { particles } = buildWelcomeHalftoneParticles( + [ + [148.5, 148.5, 119.5, fainterSourceStrokeWidth], + [148.5, 148.5, 119.5, denserSourceStrokeWidth], + ], + smallCanvasWidth, + smallCanvasHeight, + ); + + expect(particles[0].strokeWidth).toBe(MINIMUM_STROKE_WIDTH); + expect(particles[1].strokeWidth).toBe(MINIMUM_STROKE_WIDTH); + expect(particles[0].dashLength).toBeCloseTo( + fainterSourceStrokeWidth * smallCanvasScale * 1.452, + ); + expect(particles[1].dashLength / particles[0].dashLength).toBeCloseTo( + denserSourceStrokeWidth / fainterSourceStrokeWidth, + ); + }); + + it('should point the burst direction outward from the canvas center', () => { + const { particles } = buildWelcomeHalftoneParticles( + [ + [0, 0, 119.5, 2], + [297.037, 297.037, 119.5, 2], + ], + CANVAS_WIDTH, + CANVAS_HEIGHT, + ); + + expect(particles[0].burstDirectionX).toBeLessThan(0); + expect(particles[1].burstDirectionX).toBeGreaterThan(0); + }); +}); diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/buildWelcomeHalftoneParticles.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/buildWelcomeHalftoneParticles.ts index 12723ac498..9cee630d8d 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/buildWelcomeHalftoneParticles.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/buildWelcomeHalftoneParticles.ts @@ -7,6 +7,7 @@ const VIEWBOX_CENTER_Y = 119.5; const ASSEMBLE_STAGGER_SECONDS = 0.18; const ASSEMBLE_JITTER_SECONDS = 0.12; const MINIMUM_STROKE_WIDTH = 0.6; +const HALFTONE_DASH_LENGTH_TO_STROKE_WIDTH_RATIO = 1.452; const pseudoRandomFromSeed = (seed: number) => { const noise = Math.sin(seed) * 43758.5453; @@ -44,26 +45,27 @@ export const buildWelcomeHalftoneParticles = ( ); const scatterAngle = pseudoRandomFromSeed(dashIndex * 1.3) * TAU; const scatterRadius = - halftoneSize * (0.35 + 0.5 * pseudoRandomFromSeed(dashIndex * 2.1)); + halftoneSize * (0.1 + 0.1 * pseudoRandomFromSeed(dashIndex * 2.1)); + const scaledStrokeWidth = dashStrokeWidth * viewboxToCanvasScale; + const burstDirectionX = + distanceToCenter > 0 ? (targetX - canvasCenterX) / distanceToCenter : 0; + const burstDirectionY = + distanceToCenter > 0 + ? (targetY - canvasCenterY) / distanceToCenter + : -1; return { targetX, targetY, scatterStartX: canvasCenterX + Math.cos(scatterAngle) * scatterRadius, scatterStartY: canvasCenterY + Math.sin(scatterAngle) * scatterRadius, - dashLength: Math.max(dashEndX - dashStartX, 0) * viewboxToCanvasScale, - strokeWidth: Math.max( - dashStrokeWidth * viewboxToCanvasScale, - MINIMUM_STROKE_WIDTH, + dashLength: Math.max( + (dashEndX - dashStartX) * viewboxToCanvasScale, + scaledStrokeWidth * HALFTONE_DASH_LENGTH_TO_STROKE_WIDTH_RATIO, ), - burstDirectionX: - distanceToCenter > 0 - ? (targetX - canvasCenterX) / distanceToCenter - : 0, - burstDirectionY: - distanceToCenter > 0 - ? (targetY - canvasCenterY) / distanceToCenter - : -1, + strokeWidth: Math.max(scaledStrokeWidth, MINIMUM_STROKE_WIDTH), + burstDirectionX, + burstDirectionY, distanceToCenter, assembleDelaySeconds: ASSEMBLE_STAGGER_SECONDS * (distanceToCenter / maxDistanceToCenter) + @@ -72,6 +74,7 @@ export const buildWelcomeHalftoneParticles = ( positionAtLeaveStartX: 0, positionAtLeaveStartY: 0, opacityAtLeaveStart: 0, + capsuleLengthAtLeaveStart: 0, }; }, ); diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneMainThreadController.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneMainThreadController.ts index 83dc0d23c1..01db37506a 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneMainThreadController.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneMainThreadController.ts @@ -7,7 +7,6 @@ import { createWelcomeHalftoneRenderer } from '@/onboarding/components/WelcomeOv type CreateWelcomeHalftoneMainThreadControllerOptions = { canvas: HTMLCanvasElement; dotColor: string; - dotHighlightColor: string; prefersReducedMotion: boolean; initialCanvasWidth: number; initialCanvasHeight: number; @@ -17,7 +16,6 @@ type CreateWelcomeHalftoneMainThreadControllerOptions = { export const createWelcomeHalftoneMainThreadController = ({ canvas, dotColor, - dotHighlightColor, prefersReducedMotion, initialCanvasWidth, initialCanvasHeight, @@ -54,7 +52,6 @@ export const createWelcomeHalftoneMainThreadController = ({ height: initialCanvasHeight, devicePixelRatio, color: dotColor, - highlightColor: dotHighlightColor, reducedMotion: prefersReducedMotion, }); diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneWorkerController.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneWorkerController.ts index 3e8a1b1ed9..11964f616a 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneWorkerController.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/createWelcomeHalftoneWorkerController.ts @@ -9,7 +9,6 @@ const WORKER_READY_TIMEOUT_MS = 1500; type CreateWelcomeHalftoneWorkerControllerOptions = { canvas: HTMLCanvasElement; dotColor: string; - dotHighlightColor: string; prefersReducedMotion: boolean; initialCanvasWidth: number; initialCanvasHeight: number; @@ -20,7 +19,6 @@ type CreateWelcomeHalftoneWorkerControllerOptions = { export const createWelcomeHalftoneWorkerController = ({ canvas, dotColor, - dotHighlightColor, prefersReducedMotion, initialCanvasWidth, initialCanvasHeight, @@ -80,7 +78,6 @@ export const createWelcomeHalftoneWorkerController = ({ height: initialCanvasHeight, devicePixelRatio, color: dotColor, - highlightColor: dotHighlightColor, reducedMotion: prefersReducedMotion, }, [offscreenCanvas], diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.css b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.css index 84a8ccebc0..58346d0e49 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.css +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.css @@ -1,4 +1,3 @@ :root { --welcome-dot-color: #4a38f5; - --welcome-dot-highlight: #9b91f9; } diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.worker.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.worker.ts index 88f8bbf3ef..776e71eace 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.worker.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftone.worker.ts @@ -8,7 +8,6 @@ type InitMessage = { height: number; devicePixelRatio: number; color: string; - highlightColor: string; reducedMotion: boolean; }; @@ -63,7 +62,6 @@ addEventListener( height: message.height, devicePixelRatio: message.devicePixelRatio, color: message.color, - highlightColor: message.highlightColor, reducedMotion: message.reducedMotion, }); workerSelf.postMessage({ type: 'ready' }); diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneParticle.type.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneParticle.type.ts index 4255460b84..14dd481fef 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneParticle.type.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneParticle.type.ts @@ -13,4 +13,5 @@ export type WelcomeHalftoneParticle = { positionAtLeaveStartX: number; positionAtLeaveStartY: number; opacityAtLeaveStart: number; + capsuleLengthAtLeaveStart: number; }; diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneRenderer.ts b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneRenderer.ts index 569fa4a82e..4ebc87a3f8 100644 --- a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneRenderer.ts +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneRenderer.ts @@ -4,18 +4,16 @@ import { buildWelcomeHalftoneParticles } from '@/onboarding/components/WelcomeOv import { createWelcomeHalftoneAnimationFrameLoop } from '@/onboarding/components/WelcomeOverlay/createWelcomeHalftoneAnimationFrameLoop'; import { type WelcomeHalftoneParticle } from '@/onboarding/components/WelcomeOverlay/welcomeHalftoneParticle.type'; -const DASH_ASSEMBLE_DURATION_SECONDS = 0.62; +const DASH_ASSEMBLE_DURATION_SECONDS = 0.9; +const DASH_STRETCH_START_ASSEMBLE_PROGRESS = 0.35; const BURST_DURATION_SECONDS = 0.75; const SETTLE_DRIFT_DURATION_SECONDS = 0.6; -const SHIMMER_BAND_HALF_WIDTH = 0.028; const MINIMUM_VISIBLE_OPACITY = 0.01; const clampToUnitRange = (value: number) => Math.max(0, Math.min(1, value)); const interpolate = (from: number, to: number, ratio: number) => from + (to - from) * ratio; const easeOutCubic = (ratio: number) => 1 - Math.pow(1 - ratio, 3); -const easeOutExpo = (ratio: number) => - ratio >= 1 ? 1 : 1 - Math.pow(2, -10 * ratio); const smootherStep = (ratio: number) => ratio * ratio * (3 - 2 * ratio); type WelcomeHalftoneRendererOptions = { @@ -25,7 +23,6 @@ type WelcomeHalftoneRendererOptions = { height: number; devicePixelRatio: number; color: string; - highlightColor: string; reducedMotion: boolean; }; @@ -34,7 +31,6 @@ export const createWelcomeHalftoneRenderer = ( ) => { const { context, dashes, reducedMotion } = options; const baseColor = options.color; - const highlightColor = options.highlightColor; let canvasWidth = options.width; let canvasHeight = options.height; @@ -67,7 +63,7 @@ export const createWelcomeHalftoneRenderer = ( particle: WelcomeHalftoneParticle, elapsedSeconds: number, ) => { - const assembleProgress = easeOutExpo( + const assembleProgress = easeOutCubic( clampToUnitRange( (elapsedSeconds - particle.assembleDelaySeconds) / DASH_ASSEMBLE_DURATION_SECONDS, @@ -83,20 +79,30 @@ export const createWelcomeHalftoneRenderer = ( particle.targetY, assembleProgress, ); + const secondsSinceLanding = + elapsedSeconds - + (particle.assembleDelaySeconds + DASH_ASSEMBLE_DURATION_SECONDS); const settleDriftProgress = clampToUnitRange( - (elapsedSeconds - - (particle.assembleDelaySeconds + DASH_ASSEMBLE_DURATION_SECONDS)) / - SETTLE_DRIFT_DURATION_SECONDS, + secondsSinceLanding / SETTLE_DRIFT_DURATION_SECONDS, ); particleX += - Math.sin(elapsedSeconds * 1.6 + particle.driftPhase) * + Math.sin(elapsedSeconds * 1.1 + particle.driftPhase) * settleDriftProgress * - 1.2; + 0.5; particleY += - Math.cos(elapsedSeconds * 1.4 + particle.driftPhase) * + Math.cos(elapsedSeconds * 0.95 + particle.driftPhase) * settleDriftProgress * - 0.8; - return { particleX, particleY, opacity: assembleProgress }; + 0.35; + const stretchProgress = clampToUnitRange( + (assembleProgress - DASH_STRETCH_START_ASSEMBLE_PROGRESS) / + (1 - DASH_STRETCH_START_ASSEMBLE_PROGRESS), + ); + return { + particleX, + particleY, + opacity: assembleProgress, + capsuleLength: particle.dashLength * stretchProgress, + }; }; const drawFrame = (timeMs: number) => { @@ -112,11 +118,13 @@ export const createWelcomeHalftoneRenderer = ( particle.positionAtLeaveStartX = particle.targetX; particle.positionAtLeaveStartY = particle.targetY; particle.opacityAtLeaveStart = 1; + particle.capsuleLengthAtLeaveStart = particle.dashLength; } else { const assembleState = computeAssembleState(particle, elapsedSeconds); particle.positionAtLeaveStartX = assembleState.particleX; particle.positionAtLeaveStartY = assembleState.particleY; particle.opacityAtLeaveStart = assembleState.opacity; + particle.capsuleLengthAtLeaveStart = assembleState.capsuleLength; } } } @@ -130,18 +138,13 @@ export const createWelcomeHalftoneRenderer = ( context.clearRect(0, 0, canvasWidth, canvasHeight); - const shimmerSweepPosition = ((elapsedSeconds * 0.5) % 1.3) / 1.3; - const isShimmerActive = !reducedMotion && !isLeaving; - const inverseViewportSpan = 1 / (canvasWidth + canvasHeight); - let currentStrokeColor = ''; + context.strokeStyle = baseColor; for (const particle of particles) { let particleX: number; let particleY: number; let particleOpacity: number; let capsuleLength = particle.dashLength; - let capsuleDirectionX = 1; - let capsuleDirectionY = 0; if (reducedMotion) { particleX = particle.targetX; @@ -152,12 +155,12 @@ export const createWelcomeHalftoneRenderer = ( particleX = assembleState.particleX; particleY = assembleState.particleY; particleOpacity = assembleState.opacity; + capsuleLength = assembleState.capsuleLength; } else { - const easedBurstProgress = smootherStep(burstProgress); const outwardPushDistance = - easedBurstProgress * + smootherStep(burstProgress) * halftoneSize * - 1.1 * + 0.25 * (0.6 + 0.8 * (particle.distanceToCenter / maxDistanceToCenter)); particleX = particle.positionAtLeaveStartX + @@ -168,34 +171,19 @@ export const createWelcomeHalftoneRenderer = ( particleOpacity = particle.opacityAtLeaveStart * (1 - easeOutCubic(clampToUnitRange(burstProgress / 0.85))); - capsuleLength = - particle.dashLength + outwardPushDistance * 0.12 * burstProgress; - capsuleDirectionX = particle.burstDirectionX; - capsuleDirectionY = particle.burstDirectionY; + capsuleLength = particle.capsuleLengthAtLeaveStart; } if (particleOpacity <= MINIMUM_VISIBLE_OPACITY) { continue; } - const isParticleInShimmerBand = - isShimmerActive && - Math.abs( - (particleX + particleY) * inverseViewportSpan - shimmerSweepPosition, - ) < SHIMMER_BAND_HALF_WIDTH; - const strokeColor = isParticleInShimmerBand ? highlightColor : baseColor; - if (strokeColor !== currentStrokeColor) { - context.strokeStyle = strokeColor; - currentStrokeColor = strokeColor; - } - context.globalAlpha = particleOpacity; context.lineWidth = particle.strokeWidth; context.beginPath(); - const halfCapsuleX = (capsuleDirectionX * capsuleLength) / 2; - const halfCapsuleY = (capsuleDirectionY * capsuleLength) / 2; - context.moveTo(particleX - halfCapsuleX, particleY - halfCapsuleY); - context.lineTo(particleX + halfCapsuleX, particleY + halfCapsuleY); + const halfCapsuleLength = capsuleLength / 2; + context.moveTo(particleX - halfCapsuleLength, particleY); + context.lineTo(particleX + halfCapsuleLength, particleY); context.stroke(); }