Skip to content

Commit

Permalink
Implement island zooming and island overlay change (#223)
Browse files Browse the repository at this point in the history
Resolves #138 

What has been done:
- Island zooming on controls (zoom-in and zoom-out on click)
- Island overlay changing
- Set default island overlay to `dark`
- Some code refactor

Known issues (not blockers):
- Zooming is not smooth #224
  • Loading branch information
jagodarybacka authored Sep 28, 2023
2 parents f3367dc + 74b51e5 commit ee67b77
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 34 deletions.
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}
/>
</>
)
}

0 comments on commit ee67b77

Please sign in to comment.