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();
}