refactor: 얇게 남아있던 별의 꼬리가 사라지도록 수정

This commit is contained in:
2026-02-14 22:21:24 +09:00
parent 1ccb9e517a
commit 25a4ebd342
2 changed files with 54 additions and 16 deletions

View File

@@ -13,6 +13,7 @@ import {
shouldRecycleFlightStar, shouldRecycleFlightStar,
} from "@/features/flight-starfield/model/starfieldModel"; } from "@/features/flight-starfield/model/starfieldModel";
import { FlightStar } from "@/features/flight-starfield/model/types"; import { FlightStar } from "@/features/flight-starfield/model/types";
import { FLIGHT_STARFIELD_TUNING } from "@/shared/config/starfield";
import { clamp } from "@/shared/lib/math/number"; import { clamp } from "@/shared/lib/math/number";
import { getPrefersReducedMotionMediaQuery } from "@/shared/lib/motion/prefersReducedMotion"; import { getPrefersReducedMotionMediaQuery } from "@/shared/lib/motion/prefersReducedMotion";
@@ -39,6 +40,7 @@ export function FlightStarfieldCanvas({
let width = window.innerWidth; let width = window.innerWidth;
let height = window.innerHeight; let height = window.innerHeight;
let animationFrameId = 0; let animationFrameId = 0;
let dpr = 1;
const motionQuery = getPrefersReducedMotionMediaQuery(); const motionQuery = getPrefersReducedMotionMediaQuery();
let prefersReducedMotion = motionQuery.matches; let prefersReducedMotion = motionQuery.matches;
@@ -48,12 +50,21 @@ export function FlightStarfieldCanvas({
const vanishXJitter = vanishXJitterRef.current ?? 0; const vanishXJitter = vanishXJitterRef.current ?? 0;
const setCanvasSize = () => { const setCanvasSize = () => {
width = window.innerWidth; const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
height = window.innerHeight; const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
canvas.width = width; width = Math.max(1, Math.floor(viewportWidth));
canvas.height = height; height = Math.max(1, Math.floor(viewportHeight));
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(width * dpr));
canvas.height = Math.max(1, Math.floor(height * dpr));
context.setTransform(dpr, 0, 0, dpr, 0, 0);
context.imageSmoothingEnabled = false;
}; };
const snapToDevicePixel = (value: number) => Math.round(value * dpr) / dpr;
const getVanishingPoint = () => const getVanishingPoint = () =>
createVanishingPoint({ createVanishingPoint({
width, width,
@@ -102,12 +113,20 @@ export function FlightStarfieldCanvas({
const deltaX = toX - fromX; const deltaX = toX - fromX;
const deltaY = toY - fromY; const deltaY = toY - fromY;
const movementLength = Math.hypot(deltaX, deltaY); const movementLength = Math.hypot(deltaX, deltaY);
const tailLineWidth = clamp(star.radius * 0.9, 0.65, 1.6);
const canDrawTail =
star.tailLength >= FLIGHT_STARFIELD_TUNING.tail.cleanup.minTailLengthToDraw &&
movementLength >= FLIGHT_STARFIELD_TUNING.tail.cleanup.minMovementToDraw &&
visibleAlpha >= FLIGHT_STARFIELD_TUNING.tail.cleanup.minAlphaToDraw &&
tailLineWidth >= FLIGHT_STARFIELD_TUNING.tail.cleanup.minLineWidthToDraw;
if (star.tailLength < 1 || movementLength < 0.001) { if (!canDrawTail) {
const snappedToX = snapToDevicePixel(toX);
const snappedToY = snapToDevicePixel(toY);
context.globalAlpha = visibleAlpha; context.globalAlpha = visibleAlpha;
context.fillStyle = "#f8fbff"; context.fillStyle = "#f8fbff";
context.beginPath(); context.beginPath();
context.arc(toX, toY, star.radius, 0, Math.PI * 2); context.arc(snappedToX, snappedToY, star.radius, 0, Math.PI * 2);
context.fill(); context.fill();
context.globalAlpha = 1; context.globalAlpha = 1;
return; return;
@@ -115,26 +134,33 @@ export function FlightStarfieldCanvas({
const directionX = deltaX / movementLength; const directionX = deltaX / movementLength;
const directionY = deltaY / movementLength; const directionY = deltaY / movementLength;
const tailX = toX - directionX * star.tailLength; const tailX = snapToDevicePixel(toX - directionX * star.tailLength);
const tailY = toY - directionY * star.tailLength; const tailY = snapToDevicePixel(toY - directionY * star.tailLength);
const snappedToX = snapToDevicePixel(toX);
const snappedToY = snapToDevicePixel(toY);
const gradient = context.createLinearGradient(tailX, tailY, toX, toY); const gradient = context.createLinearGradient(
tailX,
tailY,
snappedToX,
snappedToY,
);
gradient.addColorStop(0, "rgba(248, 251, 255, 0)"); gradient.addColorStop(0, "rgba(248, 251, 255, 0)");
gradient.addColorStop(1, `rgba(248, 251, 255, ${visibleAlpha})`); gradient.addColorStop(1, `rgba(248, 251, 255, ${visibleAlpha})`);
context.strokeStyle = gradient; context.strokeStyle = gradient;
context.lineWidth = clamp(star.radius * 0.9, 0.65, 1.6); context.lineWidth = tailLineWidth;
context.beginPath(); context.beginPath();
context.moveTo(tailX, tailY); context.moveTo(tailX, tailY);
context.lineTo(toX, toY); context.lineTo(snappedToX, snappedToY);
context.stroke(); context.stroke();
context.globalAlpha = Math.min(1, visibleAlpha + 0.08); context.globalAlpha = Math.min(1, visibleAlpha + 0.08);
context.fillStyle = "#f8fbff"; context.fillStyle = "#f8fbff";
context.beginPath(); context.beginPath();
context.arc( context.arc(
toX, snappedToX,
toY, snappedToY,
clamp(star.radius * 0.72, 0.6, 1.45), clamp(star.radius * 0.72, 0.6, 1.45),
0, 0,
Math.PI * 2, Math.PI * 2,
@@ -177,7 +203,12 @@ export function FlightStarfieldCanvas({
}; };
const drawFrame = (moveStars: boolean) => { const drawFrame = (moveStars: boolean) => {
context.fillStyle = "rgba(2, 5, 10, 0.3)"; context.setTransform(1, 0, 0, 1, 0, 0);
context.globalAlpha = 1;
context.globalCompositeOperation = "source-over";
context.clearRect(0, 0, canvas.width, canvas.height);
context.setTransform(dpr, 0, 0, dpr, 0, 0);
context.fillStyle = "rgb(2, 5, 10)";
context.fillRect(0, 0, width, height); context.fillRect(0, 0, width, height);
drawCenterVeil(); drawCenterVeil();
@@ -235,7 +266,6 @@ export function FlightStarfieldCanvas({
}; };
const renderStatic = () => { const renderStatic = () => {
context.clearRect(0, 0, width, height);
drawFrame(false); drawFrame(false);
}; };
@@ -264,6 +294,7 @@ export function FlightStarfieldCanvas({
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
window.visualViewport?.addEventListener("resize", handleResize);
motionQuery.addEventListener("change", handleMotionChange); motionQuery.addEventListener("change", handleMotionChange);
if (prefersReducedMotion || isPaused) { if (prefersReducedMotion || isPaused) {
@@ -274,6 +305,7 @@ export function FlightStarfieldCanvas({
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
window.visualViewport?.removeEventListener("resize", handleResize);
motionQuery.removeEventListener("change", handleMotionChange); motionQuery.removeEventListener("change", handleMotionChange);
stopAnimation(); stopAnimation();
}; };
@@ -282,7 +314,7 @@ export function FlightStarfieldCanvas({
return ( return (
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="fixed inset-0 z-0 bg-black pointer-events-none" className="fixed inset-0 z-0 h-screen w-screen bg-black pointer-events-none"
/> />
); );
} }

View File

@@ -17,6 +17,12 @@ export const FLIGHT_STARFIELD_TUNING = {
pointRange: { min: 0, max: 2.5 }, pointRange: { min: 0, max: 2.5 },
shortRange: { min: 2.5, max: 3.8 }, shortRange: { min: 2.5, max: 3.8 },
longRange: { min: 4, max: 10 }, longRange: { min: 4, max: 10 },
cleanup: {
minAlphaToDraw: 0.09,
minMovementToDraw: 0.18,
minLineWidthToDraw: 1,
minTailLengthToDraw: 1,
},
}, },
spawnRadius: { spawnRadius: {
centerChance: 0.08, centerChance: 0.08,