강물 일러스트 텍스처 적용
- public/assets/water.jpg (원본 2.7MB → 1200px JPEG 233KB 최적화) - 물 타일 영역을 클리핑해 텍스처 채움, 강 방향 느린 드리프트 애니메이션 - 타일 반짝임은 은은한 화이트 오버레이로 유지, 로드 전 단색 폴백 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
바이너리 파일은 표시되지 않음.
|
이후 너비: | 높이: | 크기: 228 KiB |
@@ -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);
|
||||||
|
|
||||||
// 하늘 배경 없음(투명) — 타일부터
|
// 하늘 배경 없음(투명) — 타일부터
|
||||||
for (let j = 0; j < ROWS; j++) {
|
const tilePath = (i: number, j: number) => {
|
||||||
for (let i = 0; i < COLS; i++) {
|
|
||||||
const x = isoX(i, j);
|
const x = isoX(i, j);
|
||||||
const y = isoY(i, j);
|
const y = isoY(i, j);
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
ctx.moveTo(x, y);
|
||||||
ctx.lineTo(x + TW / 2, y + TH / 2);
|
ctx.lineTo(x + TW / 2, y + TH / 2);
|
||||||
ctx.lineTo(x, y + TH);
|
ctx.lineTo(x, y + TH);
|
||||||
ctx.lineTo(x - TW / 2, y + TH / 2);
|
ctx.lineTo(x - TW / 2, y + TH / 2);
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
if (isWater(i, j)) {
|
};
|
||||||
const shimmer = 0.85 + 0.15 * Math.sin(now / 700 + j);
|
|
||||||
ctx.fillStyle = `rgba(96, 165, 250, ${shimmer})`;
|
for (let j = 0; j < ROWS; j++) {
|
||||||
} else {
|
for (let i = 0; i < COLS; i++) {
|
||||||
|
if (isWater(i, j)) continue;
|
||||||
|
ctx.beginPath();
|
||||||
|
tilePath(i, j);
|
||||||
ctx.fillStyle = (i + j) % 2 === 0 ? "#86c66b" : "#7dbd62";
|
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[] = [];
|
||||||
|
|||||||
새 이슈에서 참조
사용자 차단