Skip to content

Commit

Permalink
Merge pull request #346 from Twinsteak/feat/floormap-canvas
Browse files Browse the repository at this point in the history
[feat] 배치도 구역을 캔버스로 렌더링하도록 변경
  • Loading branch information
EATSTEAK authored Aug 15, 2024
2 parents d8a1e2e + bf19eea commit e22aa71
Show file tree
Hide file tree
Showing 37 changed files with 776 additions and 402 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/aws-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
python-version: "3.10"
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
- uses: aws-actions/setup-sam@v2
- uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
- name: Enable corepack
run: corepack enable
- name: Install pnpm
Expand Down
9 changes: 6 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/forms": "^0.5.7",
"@types/lodash.isequal": "^4.5.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-svelte": "^2.42.0",
"eslint-plugin-tailwindcss": "^3.17.4",
"konva": "^9.3.13",
"postcss": "^8.4.39",
"postcss-load-config": "^5.1.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"svelte": "^4.2.18",
"svelte-check": "^3.8.4",
"svelte-konva": "^0.3.1",
"svelte-preprocess": "^6.0.2",
"tailwindcss": "^3.4.5",
"tslib": "^2.6.3",
Expand All @@ -46,6 +48,7 @@
},
"type": "module",
"dependencies": {
"canvas": "^2.11.2",
"lodash.isequal": "^4.5.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.10/xlsx-0.18.10.tgz"
}
Expand Down
54 changes: 0 additions & 54 deletions packages/client/src/components/atom/FloorMap.svelte

This file was deleted.

259 changes: 259 additions & 0 deletions packages/client/src/components/atom/MapCanvas.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import {
Image,
type KonvaDragTransformEvent,
type KonvaTouchEvent,
type KonvaWheelEvent,
Layer,
Stage,
} from 'svelte-konva';
import type { Stage as StageHandle } from 'konva/lib/Stage';
import { beforeUpdate, onMount } from 'svelte';
import Skeleton from './Skeleton.svelte';
import Button from './Button.svelte';
import ArrowClockwise from '../../icons/ArrowClockwise.svelte';
import Konva from 'konva';
import type { IFrame } from 'konva/lib/types';
import { sineInOut } from 'svelte/easing';
let clazz = '';
export { clazz as class };
export let alt = '배치도';
export let src: string;
export let highlightSrc: string = null;
export let highlightX: number = 0;
export let highlightY: number = 0;
let parent: HTMLDivElement;
let stage: StageHandle;
let image: HTMLImageElement;
let highlightImage: HTMLImageElement;
let highlight: Konva.Image;
let width: number;
let height: number;
let resizeCallback: NodeJS.Timeout;
let imageRatio: number;
let zoomScale = 1;
let stageX = 0;
let stageY = 0;
let defaultScale: number;
let mounted: boolean = false;
let highlightAnimation: Konva.Animation = null;
$: canvasRatio = width && height && width / height;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function resizeCanvas(_entries: ResizeObserverEntry[], _observer: ResizeObserver) {
if (resizeCallback) clearTimeout(resizeCallback);
if (parent && (width !== parent.clientWidth || height !== parent.clientHeight)) {
width = undefined;
height = undefined;
resizeCallback = setTimeout(function() {
if (parent) {
width = parent?.clientWidth;
height = parent?.clientHeight;
}
resizeCallback = undefined;
}, 500);
}
}
function getDistance(p1: { x: number, y: number }, p2: { x: number, y: number }): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function getCenter(p1: { x: number, y: number }, p2: { x: number, y: number }): { x: number, y: number } {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}
let dragStopped: boolean = false;
let lastCenter: { x: number, y: number } = null;
let lastDist: number = null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function resetTouchPosition(_e: KonvaTouchEvent) {
lastCenter = null;
lastDist = null;
}
function zoomMapWithTouch(e: KonvaTouchEvent): void {
e.detail.evt.preventDefault();
const touch1 = e.detail.evt.touches[0];
const touch2 = e.detail.evt.touches[1];
if (touch1 && !touch2 && !stage.isDragging() && dragStopped) {
stage.startDrag();
dragStopped = false;
}
if (touch1 && touch2) {
if (!dragStopped) {
dragStopped = true;
stage.stopDrag();
}
const p1 = { x: touch1.clientX, y: touch1.clientY };
const p2 = { x: touch2.clientX, y: touch2.clientY };
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
const newCenter = getCenter(p1, p2);
const dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
const oldScale = zoomScale;
const pointTo = {
x: (newCenter.x - stage.x()) / oldScale,
y: (newCenter.y - stage.y()) / oldScale,
};
console.log(oldScale, ((dist / lastDist)));
const newScale = oldScale * (dist / lastDist);
const dx = newCenter.x - lastCenter.x;
const dy = newCenter.y - lastCenter.y;
const newPos = {
x: newCenter.x - pointTo.x * newScale + dx,
y: newCenter.y - pointTo.y * newScale + dy,
};
if (newScale <= defaultScale * 4.0 && newScale >= defaultScale * 0.25) {
zoomScale = newScale;
}
stageX = newPos.x;
stageY = newPos.y;
lastDist = dist;
lastCenter = newCenter;
}
}
function zoomMapWithWheel(e: KonvaWheelEvent): void {
// Enable zoom only when user scroll with ctrl key
if (e.detail.evt.ctrlKey) {
e.detail.evt.preventDefault();
let oldScale = zoomScale;
const pointer = stage.getPointerPosition();
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
let direction = e.detail.evt.deltaY > 0 ? -1 : 1;
let newScale = direction > 0 ? oldScale * 1.1 : oldScale / 1.1;
// Allow zoom level from 0.25 to 4.0
if (newScale <= defaultScale * 4.0 && newScale >= defaultScale * 0.25) {
zoomScale = newScale;
stageX = pointer.x - mousePointTo.x * zoomScale;
stageY = pointer.y - mousePointTo.y * zoomScale;
}
}
}
function reset() {
stageX = (width && image.width && defaultScale) ? (width - (image.width * defaultScale)) / 2 : 0;
stageY = 0;
zoomScale = defaultScale;
}
function updateStageCoords(e: KonvaDragTransformEvent): void {
stageX = e.detail.target.x();
stageY = e.detail.target.y();
}
onMount(() => {
width = parent?.clientWidth;
height = parent?.clientHeight;
new ResizeObserver(resizeCanvas).observe(parent);
mounted = true;
});
beforeUpdate(() => {
if (highlightAnimation) highlightAnimation.stop();
highlightAnimation = null;
});
$: if (mounted && src) {
const img = document.createElement('img');
img.src = src;
img.onload = () => {
image = img;
imageRatio = img.width / img.height;
};
}
$: if (mounted && highlightSrc) {
const highlightImg = document.createElement('img');
highlightImg.src = highlightSrc;
highlightImg.onload = () => {
highlightImage = highlightImg;
};
// TODO: move camera to highlighted position
}
const easing = (currentTime: number, repeatTime: number) => {
const flowVal = Math.abs((currentTime % repeatTime) - (repeatTime / 2)) / (repeatTime / 2); // 0~1
return sineInOut(flowVal);
};
$: if (highlight) {
if (highlightAnimation) highlightAnimation.stop();
highlightAnimation = new Konva.Animation((frame: IFrame) => {
highlight.setAttr('opacity', (easing(frame.time, 3000) * 0.8) + 0.2);
});
highlightAnimation.start();
}
$: if (imageRatio && canvasRatio) {
defaultScale = canvasRatio >= imageRatio ? height / image.height : width / image.width;
zoomScale = defaultScale;
reset();
}
</script>

<div
bind:this={parent}
class="{clazz} relative h-full w-full cursor-grab"
in:fly={{ y: 100, duration: 300 }}
aria-label={alt}>
{#if !isNaN(width) && !isNaN(height)}
{#key `${width};${height}`}
<Stage
bind:handle={stage}
config={{ width, height, scale: { x: zoomScale, y: zoomScale }, x: stageX, y: stageY, draggable: true }}
on:wheel={zoomMapWithWheel}
on:dragend={updateStageCoords}
on:touchmove={zoomMapWithTouch}
on:touchend={resetTouchPosition}
>
<Layer>
<Image config={{ image, x: 0, y: 0 }}></Image>
{#if highlightImage}
<Image config={{ image: highlightImage, x: highlightX, y: highlightY }} bind:handle={highlight}></Image>
{/if}
</Layer>
</Stage>
{/key}
{#if stageX !== 0 || stageY !== 0 || zoomScale !== defaultScale}
<Button class="absolute bottom-0 right-0 m-4 bg-white" on:click={reset}>
<ArrowClockwise />
</Button>
{/if}
{:else}
<Skeleton class="w-full h-full rounded-xl bg-gray-300" />
{/if}
</div>
Loading

0 comments on commit e22aa71

Please sign in to comment.