강물 일러스트 텍스처 적용

- public/assets/water.jpg (원본 2.7MB → 1200px JPEG 233KB 최적화)
- 물 타일 영역을 클리핑해 텍스처 채움, 강 방향 느린 드리프트 애니메이션
- 타일 반짝임은 은은한 화이트 오버레이로 유지, 로드 전 단색 폴백

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 12:25:39 +09:00
co-authored by Claude Fable 5
부모 b50a22c6b9
커밋 ccf9052dc9
2개의 변경된 파일과 65개의 추가작업 그리고 13개의 파일을 삭제
바이너리 파일은 표시되지 않음.

이후

너비:  |  높이:  |  크기: 228 KiB

+65 -13
파일 보기
@@ -88,6 +88,7 @@ export default function VillageView() {
const animsRef = useRef<Map<string, CatAnim>>(new Map()); const animsRef = useRef<Map<string, CatAnim>>(new Map());
const bubblesRef = useRef<Bubble[]>([]); const bubblesRef = useRef<Bubble[]>([]);
const toolImagesRef = useRef<Map<AptitudeKey, HTMLImageElement>>(new Map()); const toolImagesRef = useRef<Map<AptitudeKey, HTMLImageElement>>(new Map());
const waterImgRef = useRef<HTMLImageElement | null>(null);
const hitRectsRef = useRef< const hitRectsRef = useRef<
Array<{ id: string; x: number; y: number; w: number; h: number }> Array<{ id: string; x: number; y: number; w: number; h: number }>
>([]); >([]);
@@ -109,6 +110,12 @@ export default function VillageView() {
img.src = src; img.src = src;
img.onload = () => toolImagesRef.current.set(key as AptitudeKey, img); img.onload = () => toolImagesRef.current.set(key as AptitudeKey, img);
} }
// 강물 텍스처 (로드 전에는 단색 폴백)
{
const img = new Image();
img.src = "/assets/water.jpg";
img.onload = () => (waterImgRef.current = img);
}
let raf = 0; let raf = 0;
let last = performance.now(); let last = performance.now();
@@ -188,28 +195,73 @@ export default function VillageView() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H); ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// 하늘 배경 없음(투명) — 타일부터 // 하늘 배경 없음(투명) — 타일부터
const tilePath = (i: number, j: number) => {
const x = isoX(i, j);
const y = isoY(i, j);
ctx.moveTo(x, y);
ctx.lineTo(x + TW / 2, y + TH / 2);
ctx.lineTo(x, y + TH);
ctx.lineTo(x - TW / 2, y + TH / 2);
ctx.closePath();
};
for (let j = 0; j < ROWS; j++) { for (let j = 0; j < ROWS; j++) {
for (let i = 0; i < COLS; i++) { for (let i = 0; i < COLS; i++) {
const x = isoX(i, j); if (isWater(i, j)) continue;
const y = isoY(i, j);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, y); tilePath(i, j);
ctx.lineTo(x + TW / 2, y + TH / 2); ctx.fillStyle = (i + j) % 2 === 0 ? "#86c66b" : "#7dbd62";
ctx.lineTo(x, y + TH);
ctx.lineTo(x - TW / 2, y + TH / 2);
ctx.closePath();
if (isWater(i, j)) {
const shimmer = 0.85 + 0.15 * Math.sin(now / 700 + j);
ctx.fillStyle = `rgba(96, 165, 250, ${shimmer})`;
} else {
ctx.fillStyle = (i + j) % 2 === 0 ? "#86c66b" : "#7dbd62";
}
ctx.fill(); ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.10)"; ctx.strokeStyle = "rgba(255,255,255,0.10)";
ctx.stroke(); ctx.stroke();
} }
} }
// 강물 — 일러스트 텍스처를 물 타일 영역에 클리핑해서 채움
{
ctx.save();
ctx.beginPath();
for (let j = 0; j < ROWS; j++)
for (let i = 0; i < COLS; i++) if (isWater(i, j)) tilePath(i, j);
ctx.clip();
const water = waterImgRef.current;
if (water) {
// 물 영역 바운딩 박스를 덮도록 cover 배치 + 흐르는 방향으로 느린 드리프트
const left = isoX(COLS - 1, ROWS - 1) - TW / 2;
const right = isoX(COLS - 1, 0) + TW / 2;
const top = isoY(COLS - 1, 0);
const bottom = isoY(COLS - 1, ROWS - 1) + TH;
const boxW = right - left;
const boxH = bottom - top;
const scale = Math.max(boxW / water.width, boxH / water.height) * 1.15;
const w = water.width * scale;
const h = water.height * scale;
// 강 줄기 방향(좌하향)으로 왕복 드리프트
const drift = Math.sin(now / 4000) * 14;
ctx.drawImage(
water,
left + (boxW - w) / 2 - drift,
top + (boxH - h) / 2 + drift / 2,
w,
h,
);
} else {
ctx.fillStyle = "#60a5fa";
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
}
// 은은한 반짝임 오버레이
for (let j = 0; j < ROWS; j++) {
const glint = 0.05 + 0.05 * Math.sin(now / 700 + j);
ctx.beginPath();
tilePath(COLS - 1, j);
ctx.fillStyle = `rgba(255,255,255,${glint})`;
ctx.fill();
}
ctx.restore();
}
// 그릴 것들을 y 순으로 정렬해 겹침 처리 // 그릴 것들을 y 순으로 정렬해 겹침 처리
type Sprite = { y: number; draw: () => void }; type Sprite = { y: number; draw: () => void };
const sprites: Sprite[] = []; const sprites: Sprite[] = [];