Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement island zooming and island overlay change #223

Merged
merged 6 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/redux-state/slices/island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type IslandState = {

const initialState: IslandState = {
mode: "default",
overlay: "none",
overlay: "dark",
realms: REALMS,
stakingRealmId: null,
displayedRealmId: null,
Expand Down
40 changes: 40 additions & 0 deletions src/shared/utils/island.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import assert from "assert"
import { Stage } from "konva/lib/Stage"
import { ISLAND_BOX } from "shared/constants"

type Dimensions = {
width: number
Expand All @@ -14,6 +16,11 @@ type RealmRenderData = {
paths: { data: string }[]
}

type Coordinates = {
x: number
y: number
}

export function limitToBounds(val: number, min: number, max: number) {
if (val < min) return min
if (val > max) return max
Expand Down Expand Up @@ -110,3 +117,36 @@ export function createBackgroundMask(
ctx.drawImage(bgImage, 0, 0)
return canvas
}

export function calculateNewIslandScale(
newValue: number,
minScale: number
): number {
const newScale = limitToBounds(newValue, minScale, Math.max(0.45, minScale))
return newScale
}

export function calculateIslandPosition(
stage: Stage,
newScale: number,
targetX: number,
targetY: number
): Coordinates {
const maxX = ISLAND_BOX.width - stage.width() / newScale
const maxY = ISLAND_BOX.height - stage.height() / newScale

// Force bounds while zooming in/out
const newX = limitToBounds(targetX, -maxX * newScale, 0)
const newY = limitToBounds(targetY, -maxY * newScale, 0)

return { x: newX, y: newY }
}

export function getCurrentCanvasPosition(
positionX: number,
positionY: number,
zoom: number
): Coordinates {
const canvasPosition = { x: positionX / zoom, y: positionY / zoom }
return canvasPosition
}
42 changes: 34 additions & 8 deletions src/ui/Controls/IslandControl.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import React, { useState } from "react"
import React from "react"
import {
selectIslandOverlay,
setIslandOverlay,
useDappDispatch,
useDappSelector,
} from "redux-state"

function IslandControlIcon() {
const isOverlay = useDappSelector(selectIslandOverlay)

function IslandControlIcon({ isOverlay }: { isOverlay: boolean }) {
return (
<svg
width="32"
Expand All @@ -10,13 +18,28 @@ function IslandControlIcon({ isOverlay }: { isOverlay: boolean }) {
xmlns="http://www.w3.org/2000/svg"
style={{ position: "relative", zIndex: 2 }}
>
<circle cx="8" cy="8" r="2" fill={!isOverlay ? "#11BEA9" : "#0D2321"} />
<circle
cx="8"
cy="8"
r="2"
fill={isOverlay !== "none" ? "#11BEA9" : "#0D2321"}
/>
<circle cx="16" cy="8" r="2" fill="#0D2321" />
<circle cx="24" cy="8" r="2" fill="#0D2321" />
<circle cx="9" cy="16" r="2" fill="#0D2321" />
<circle cx="16" cy="16" r="2" fill={!isOverlay ? "#3CC5EE" : "#0D2321"} />
<circle
cx="16"
cy="16"
r="2"
fill={isOverlay !== "none" ? "#3CC5EE" : "#0D2321"}
/>
<circle cx="24" cy="16" r="2" fill="#0D2321" />
<circle cx="8" cy="24" r="2" fill={!isOverlay ? "#F2B824" : "#0D2321"} />
<circle
cx="8"
cy="24"
r="2"
fill={isOverlay !== "none" ? "#F2B824" : "#0D2321"}
/>
<circle cx="16" cy="24" r="2" fill="#0D2321" />
<circle cx="24" cy="24" r="2" fill="#0D2321" />
<defs>
Expand Down Expand Up @@ -49,16 +72,19 @@ function IslandControlIcon({ isOverlay }: { isOverlay: boolean }) {
}

export default function IslandControl() {
const [isOverlay, setIsOverlay] = useState(false)
const isOverlay = useDappSelector(selectIslandOverlay)
const dispatch = useDappDispatch()

return (
<>
<button
type="button"
className="map_control button_reset center"
onClick={() => setIsOverlay((prev) => !prev)}
onClick={() =>
dispatch(setIslandOverlay(isOverlay === "none" ? "dark" : "none"))
}
>
<IslandControlIcon isOverlay={isOverlay} />
<IslandControlIcon />
</button>
<style jsx>
{`
Expand Down
9 changes: 7 additions & 2 deletions src/ui/Controls/ZoomControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import Icon from "shared/components/Icon"

type ZoomControlProps = {
icon: string
onClick: () => void
}

export default function ZoomControl({ icon }: ZoomControlProps) {
export default function ZoomControl({ icon, onClick }: ZoomControlProps) {
return (
<>
<button className="control center button_reset" type="button">
<button
className="control center button_reset"
type="button"
onClick={onClick}
>
<div className="control_icon">
<Icon src={icon} height="16px" width="16px" />
</div>
Expand Down
56 changes: 53 additions & 3 deletions src/ui/Controls/ZoomControls.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,64 @@
import React from "react"
import zoomIn from "shared/assets/icons/m/zoom-in.svg"
import zoomOut from "shared/assets/icons/m/zoom-out.svg"
import { Stage } from "konva/lib/Stage"

import { useDappDispatch, setIslandZoomLevel } from "redux-state"
import {
calculateIslandPosition,
calculateNewIslandScale,
getCurrentCanvasPosition,
} from "shared/utils"
import ZoomControl from "./ZoomControl"

export default function ZoomControls() {
type ZoomControlsProps = {
stage: Stage
minScale: number
}

export default function ZoomControls({ stage, minScale }: ZoomControlsProps) {
const dispatch = useDappDispatch()

const zoomHandler = (increase: boolean) => {
const zoom = stage.scaleX()
const scaleBy = 1.05

const center = {
x: stage.width() / 2,
y: stage.height() / 2,
}

// Get current center related to screen
const canvasPosition = getCurrentCanvasPosition(
center.x - stage.x(),
center.y - stage.y(),
zoom
)

const newScale = calculateNewIslandScale(
increase ? zoom * scaleBy : zoom / scaleBy,
minScale
)

const newPosition = {
x: center.x - canvasPosition.x * newScale,
y: center.y - canvasPosition.y * newScale,
}

// Force bounds while zooming in/out
stage.absolutePosition(
calculateIslandPosition(stage, newScale, newPosition.x, newPosition.y)
)

// Update the stage scale
dispatch(setIslandZoomLevel(newScale))
}

return (
<>
<div className="controls column_center">
<ZoomControl icon={zoomIn} />
<ZoomControl icon={zoomOut} />
<ZoomControl icon={zoomIn} onClick={() => zoomHandler(true)} />
<ZoomControl icon={zoomOut} onClick={() => zoomHandler(false)} />
</div>
<style jsx>{`
.controls {
Expand Down
12 changes: 10 additions & 2 deletions src/ui/Controls/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import React from "react"
import Portal from "shared/components/Portal"
import { Stage } from "konva/lib/Stage"
import ZoomControls from "./ZoomControls"
import IslandControl from "./IslandControl"

export default function Controls() {
type ControlsProps = {
stage: Stage | null
minScale: number
}

export default function Controls({ stage, minScale }: ControlsProps) {
if (!stage || !minScale) return null

return (
<>
<Portal>
<div className="controls column_center">
<ZoomControls />
<ZoomControls stage={stage} minScale={minScale} />
<IslandControl />
</div>
</Portal>
Expand Down
36 changes: 18 additions & 18 deletions src/ui/Island/InteractiveIsland.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
getWindowDimensions,
getMinimumScale,
limitToBounds,
calculateNewIslandScale,
calculateIslandPosition,
getCurrentCanvasPosition,
} from "shared/utils"
import Controls from "ui/Controls"
import Background from "./Background"
Expand Down Expand Up @@ -72,38 +75,32 @@ export default function InteractiveIsland() {
const zoomFactor = 0.001
const { minScale } = settingsRef.current

const newScale = limitToBounds(
const newScale = calculateNewIslandScale(
zoom + delta * -zoomFactor,
minScale,
Math.max(0.45, minScale)
minScale
)

const stagePos = stage.absolutePosition()
const pointer = stage.getPointerPosition()

if (pointer && newScale !== zoom) {
// Get current mouse position in the canvas
const pointerCanvasPos = {
x: -(pointer.x - stagePos.x) / zoom,
y: -(pointer.y - stagePos.y) / zoom,
}

const maxX = ISLAND_BOX.width - stage.width() / newScale
const maxY = ISLAND_BOX.height - stage.height() / newScale
const pointerCanvasPos = getCurrentCanvasPosition(
-(pointer.x - stagePos.x),
-(pointer.y - stagePos.y),
zoom
)

// Add back pointer position to retrieve "same canvas position" offset
const targetX = pointerCanvasPos.x * newScale + pointer.x
const targetY = pointerCanvasPos.y * newScale + pointer.y

stage.scale({ x: newScale, y: newScale })

// Force bounds while zooming in/out
stage.absolutePosition({
x: limitToBounds(targetX, -maxX * newScale, 0),
y: limitToBounds(targetY, -maxY * newScale, 0),
})
stage.absolutePosition(
calculateIslandPosition(stage, newScale, targetX, targetY)
)

// Manually update the stage scale and queue a state update
// Update the stage scale
dispatch(setIslandZoomLevel(newScale))
}
acc = 0
Expand Down Expand Up @@ -169,7 +166,10 @@ export default function InteractiveIsland() {
<RealmPin />
</Layer>
</Stage>
<Controls />
<Controls
stage={islandRef.current}
minScale={settingsRef.current.minScale}
/>
</>
)
}