Restore the Figma halftone shapes and calm the welcome animation (#23495)
https://github.com/user-attachments/assets/0c06d190-977e-4ad6-8a02-251f341ee310 The welcome overlay settled into circles instead of the halftone from Figma: the densify step that took the dot set from 681 to 2897 emitted points, so every dash had zero length. All 681 dashes in the Figma export (`public/images/onboarding/welcome-halftone.svg`) share a constant length / stroke width ratio of 1.452, so the shape is restored by deriving the length from the stroke width, with no data regeneration. Dashes now stretch into shape while they are still flying in, rather than popping once they have landed. The rest of the pass makes the animation quieter. The shine sweep is gone, along with the highlight colour that only fed it. Particles approach from much closer on a gentler ease, the idle drift is roughly halved, and the exit is a soft outward drift instead of a burst that threw everything off screen. The white pill behind the title and the person chip's surface are removed too, so the title reads directly against the halftone. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23495?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
-4
@@ -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,
|
||||
|
||||
-18
@@ -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}
|
||||
>
|
||||
<StyledTitleSurface />
|
||||
<StyledTitleBoldRun>
|
||||
{welcomeTitleWords.map((word, index) => (
|
||||
<StyledWord
|
||||
|
||||
-5
@@ -10,11 +10,6 @@ type WelcomePersonChipSizeVariant = 'default' | 'compact';
|
||||
|
||||
const StyledChip = styled.div<{ sizeVariant: WelcomePersonChipSizeVariant }>`
|
||||
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'
|
||||
|
||||
+75
@@ -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);
|
||||
});
|
||||
});
|
||||
+16
-13
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
-3
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
-3
@@ -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],
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
:root {
|
||||
--welcome-dot-color: #4a38f5;
|
||||
--welcome-dot-highlight: #9b91f9;
|
||||
}
|
||||
|
||||
-2
@@ -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' });
|
||||
|
||||
+1
@@ -13,4 +13,5 @@ export type WelcomeHalftoneParticle = {
|
||||
positionAtLeaveStartX: number;
|
||||
positionAtLeaveStartY: number;
|
||||
opacityAtLeaveStart: number;
|
||||
capsuleLengthAtLeaveStart: number;
|
||||
};
|
||||
|
||||
+31
-43
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user