From ef5cd0cdef779dd81a4c536943340e80092331fd Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Tue, 11 Feb 2020 15:10:43 +0700 Subject: [PATCH] scaling stickers --- src/map/Map/index.tsx | 103 ++++++++++++++++++------------------- src/map/Sticker/index.tsx | 19 +++++-- src/map/Stickers/index.tsx | 68 ++++++++++++++---------- src/redux/editor/sagas.ts | 16 +----- src/redux/map/actions.ts | 5 ++ src/redux/map/constants.ts | 3 +- src/redux/map/handlers.ts | 7 +++ src/redux/map/index.ts | 2 + src/redux/map/sagas.ts | 4 ++ src/redux/map/selectors.ts | 3 +- src/redux/store.ts | 3 ++ src/styles/stickers.less | 5 ++ src/utils/renderer.ts | 87 ++++++++++++++++--------------- 13 files changed, 181 insertions(+), 144 deletions(-) diff --git a/src/map/Map/index.tsx b/src/map/Map/index.tsx index 2c3b10a..44cf90b 100644 --- a/src/map/Map/index.tsx +++ b/src/map/Map/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo } from 'react'; import { MainMap } from '~/constants/map'; import { createPortal } from 'react-dom'; @@ -18,7 +18,6 @@ import 'leaflet/dist/leaflet.css'; import { selectEditorEditing, selectEditorMode, selectEditorGpx } from '~/redux/editor/selectors'; import { MODES } from '~/constants/modes'; import { selectUserLocation } from '~/redux/user/selectors'; -import { GPX_ROUTE_COLORS } from '~/redux/editor/constants'; const mapStateToProps = state => ({ provider: selectMapProvider(state), @@ -41,67 +40,63 @@ type IProps = React.HTMLAttributes & ReturnType & typeof mapDispatchToProps & {}; -const MapUnconnected: React.FC = ({ - provider, - stickers, - editing, - mode, - location, - gpx, +const MapUnconnected: React.FC = memo( + ({ + provider, + stickers, + editing, + mode, + location, + gpx, - mapClicked, - mapSetSticker, - mapDropSticker, -}) => { - const onClick = React.useCallback( - event => { - if (!MainMap.clickable || mode === MODES.NONE) return; + mapClicked, + mapSetSticker, + mapDropSticker, + }) => { + const onClick = React.useCallback( + event => { + if (!MainMap.clickable || mode === MODES.NONE) return; - mapClicked(event.latlng); - }, - [mapClicked, mode] - ); + mapClicked(event.latlng); + }, + [mapClicked, mode] + ); - React.useEffect(() => { - MainMap.addEventListener('click', onClick); + React.useEffect(() => { + MainMap.addEventListener('click', onClick); - return () => { - MainMap.removeEventListener('click', onClick); - }; - }, [MainMap, onClick]); + return () => { + MainMap.removeEventListener('click', onClick); + }; + }, [MainMap, onClick]); - return createPortal( -
- + return createPortal( +
+ - + - - + + - - + + - {gpx.enabled && - gpx.list.map( - ({ latlngs, enabled, color }, index) => - enabled && ( - - ) - )} -
, - document.getElementById('canvas') - ); -}; + {gpx.enabled && + gpx.list.map( + ({ latlngs, enabled, color }, index) => + enabled && + )} +
, + document.getElementById('canvas') + ); + } +); const Map = connect(mapStateToProps, mapDispatchToProps)(MapUnconnected); export { Map }; diff --git a/src/map/Sticker/index.tsx b/src/map/Sticker/index.tsx index 9c9d8d0..25f9148 100644 --- a/src/map/Sticker/index.tsx +++ b/src/map/Sticker/index.tsx @@ -13,6 +13,7 @@ interface IProps { onDragStart?: () => void; index: number; is_editing: boolean; + zoom: number; mapSetSticker: (index: number, sticker: IStickerDump) => void; mapDropSticker: (index: number) => void; @@ -29,13 +30,16 @@ const getX = e => const Sticker: React.FC = ({ sticker, index, + is_editing, + zoom, mapSetSticker, mapDropSticker, - is_editing, }) => { const [text, setText] = useState(sticker.text); const [layer, setLayer] = React.useState(null); const [dragging, setDragging] = React.useState(false); + const wrapper = useRef(null); + let angle = useRef(sticker.angle); const element = React.useMemo(() => document.createElement('div'), []); @@ -146,6 +150,14 @@ const Sticker: React.FC = ({ setText(sticker.text); }, [layer, sticker.text]); + useEffect(() => { + if (!wrapper || !wrapper.current) return; + + const scale = zoom / 13; + + wrapper.current.style.transform = `scale(${scale}) perspective(1px)` + }, [zoom, wrapper]); + // Attaches onMoveFinished event to item React.useEffect(() => { if (!layer) return; @@ -200,8 +212,9 @@ const Sticker: React.FC = ({ element.className = is_editing ? 'sticker-container' : 'sticker-container inactive'; }, [element, is_editing, layer]); + return createPortal( - +
@@ -220,7 +233,7 @@ const Sticker: React.FC = ({
- , +
, element ); }; diff --git a/src/map/Stickers/index.tsx b/src/map/Stickers/index.tsx index 93a9850..52170d1 100644 --- a/src/map/Stickers/index.tsx +++ b/src/map/Stickers/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, memo, FC, useEffect, useCallback } from 'react'; import { IStickerDump } from '~/redux/map/types'; -import { FeatureGroup, Map } from 'leaflet'; +import { FeatureGroup, Map, LeafletEvent } from 'leaflet'; import { Sticker } from '~/map/Sticker'; import { mapSetSticker, mapDropSticker } from '~/redux/map/actions'; import { MainMap } from '~/constants/map'; @@ -8,39 +8,51 @@ import { MainMap } from '~/constants/map'; interface IProps { stickers: IStickerDump[]; is_editing: boolean; + mapSetSticker: typeof mapSetSticker; mapDropSticker: typeof mapDropSticker; } -const Stickers: React.FC = React.memo( - ({ stickers, is_editing, mapSetSticker, mapDropSticker }) => { - const [layer, setLayer] = React.useState(null); +const Stickers: FC = memo(({ stickers, is_editing, mapSetSticker, mapDropSticker }) => { + const [layer, setLayer] = useState(null); + const [zoom, setZoom] = useState(16); - React.useEffect(() => { - if (!MainMap) return; + const onZoomChange = useCallback( + (event: LeafletEvent) => { + setZoom(event.target._zoom); + }, + [setZoom] + ); - const item = new FeatureGroup().addTo(MainMap.stickerLayer); - setLayer(item); + useEffect(() => { + if (!MainMap) return; - return () => MainMap.stickerLayer.removeLayer(item); - }, [MainMap]); + const item = new FeatureGroup().addTo(MainMap.stickerLayer); + setLayer(item); + MainMap.on('zoomend', onZoomChange); - return ( -
- {layer && - stickers.map((sticker, index) => ( - - ))} -
- ); - } -); + return () => { + MainMap.off('zoomend', onZoomChange); + MainMap.stickerLayer.removeLayer(item); + }; + }, [MainMap, onZoomChange]); + + return ( +
+ {layer && + stickers.map((sticker, index) => ( + + ))} +
+ ); +}); export { Stickers }; diff --git a/src/redux/editor/sagas.ts b/src/redux/editor/sagas.ts index 057f42d..77a5f42 100644 --- a/src/redux/editor/sagas.ts +++ b/src/redux/editor/sagas.ts @@ -139,23 +139,9 @@ function* getRenderData() { yield composeImages({ geometry, images, ctx }); yield composePoly({ points, ctx }); - // TODO: make additional dashed lines - // gpx.list.forEach(item => { - // if (!gpx.enabled || !item.enabled || !item.latlngs.length) return; - - // composePoly({ - // points: getPolyPlacement(item.latlngs), - // ctx, - // color: item.color, - // opacity: 0.5, - // weight: 9, - // dash: [12, 12], - // }); - // }); - yield composeArrows({ points, ctx }); yield composeDistMark({ ctx, points, distance }); - yield composeStickers({ stickers: sticker_points, ctx }); + yield composeStickers({ stickers: sticker_points, ctx, zoom: MainMap.getZoom() / 13 }); yield put(editorSetRenderer({ info: 'Готово', progress: 1 })); diff --git a/src/redux/map/actions.ts b/src/redux/map/actions.ts index 5d4a36c..0603f88 100644 --- a/src/redux/map/actions.ts +++ b/src/redux/map/actions.ts @@ -79,3 +79,8 @@ export const mapSetAddressOrigin = (address_origin: IMapReducer['address_origin' type: MAP_ACTIONS.SET_ADDRESS_ORIGIN, address_origin, }); + +export const mapZoomChange = (zoom: number) => ({ + type: MAP_ACTIONS.ZOOM_CHANGE, + zoom, +}); diff --git a/src/redux/map/constants.ts b/src/redux/map/constants.ts index 253eeb9..dce5840 100644 --- a/src/redux/map/constants.ts +++ b/src/redux/map/constants.ts @@ -17,5 +17,6 @@ export const MAP_ACTIONS = { SET_STICKERS: `${P}-SET_STICKERS`, DROP_STICKER: `${P}-DROP_STICKER`, - MAP_CLICKED: `${P}-MAP_CLICKED` + MAP_CLICKED: `${P}-MAP_CLICKED`, + ZOOM_CHANGE: `${P}-ZOOM_CHANGE` } \ No newline at end of file diff --git a/src/redux/map/handlers.ts b/src/redux/map/handlers.ts index 144ef83..ac6305f 100644 --- a/src/redux/map/handlers.ts +++ b/src/redux/map/handlers.ts @@ -14,6 +14,7 @@ import { mapSetLogo, mapSetAddressOrigin, mapSetStickers, + mapZoomChange, } from './actions'; const setMap = (state: IMapReducer, { map }: ReturnType): IMapReducer => ({ @@ -101,6 +102,11 @@ const setAddressOrigin = (state, { address_origin }: ReturnType): IMapReducer => ({ + ...state, + zoom +}); + export const MAP_HANDLERS = { [MAP_ACTIONS.SET_MAP]: setMap, [MAP_ACTIONS.SET_PROVIDER]: setProvider, @@ -116,4 +122,5 @@ export const MAP_HANDLERS = { [MAP_ACTIONS.SET_PUBLIC]: setPublic, [MAP_ACTIONS.SET_LOGO]: setLogo, [MAP_ACTIONS.SET_ADDRESS_ORIGIN]: setAddressOrigin, + [MAP_ACTIONS.ZOOM_CHANGE]: zoomChange, }; diff --git a/src/redux/map/index.ts b/src/redux/map/index.ts index daea378..1084456 100644 --- a/src/redux/map/index.ts +++ b/src/redux/map/index.ts @@ -16,6 +16,7 @@ export interface IMapReducer { description: string; owner: { id: string }; is_public: boolean; + zoom: number; } export const MAP_INITIAL_STATE: IMapReducer = { @@ -29,6 +30,7 @@ export const MAP_INITIAL_STATE: IMapReducer = { description: '', owner: { id: null }, is_public: false, + zoom: 13, } export const map = createReducer(MAP_INITIAL_STATE, MAP_HANDLERS) \ No newline at end of file diff --git a/src/redux/map/sagas.ts b/src/redux/map/sagas.ts index 9825b59..718a242 100644 --- a/src/redux/map/sagas.ts +++ b/src/redux/map/sagas.ts @@ -328,6 +328,10 @@ function* setChanged() { yield put(editorSetChanged(true)); } +function* onZoomChange() { + +} + export function* mapSaga() { yield takeEvery( [MAP_ACTIONS.SET_ROUTE, MAP_ACTIONS.SET_STICKER, MAP_ACTIONS.SET_STICKERS], diff --git a/src/redux/map/selectors.ts b/src/redux/map/selectors.ts index 543f102..7e00957 100644 --- a/src/redux/map/selectors.ts +++ b/src/redux/map/selectors.ts @@ -6,4 +6,5 @@ export const selectMapLogo = (state: IState) => state.map.logo; export const selectMapRoute= (state: IState) => state.map.route; export const selectMapStickers = (state: IState) => state.map.stickers; export const selectMapTitle= (state: IState) => state.map.title; -export const selectMapAddress = (state: IState) => state.map.address; \ No newline at end of file +export const selectMapAddress = (state: IState) => state.map.address; +export const selectMapZoom = (state: IState) => state.map.zoom; \ No newline at end of file diff --git a/src/redux/store.ts b/src/redux/store.ts index 481e355..ccdee48 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -19,6 +19,8 @@ import { mapSaga } from '~/redux/map/sagas'; import { watchLocation, getLocation } from '~/utils/window'; import { LatLngLiteral } from 'leaflet'; import { setUserLocation } from './user/actions'; +import { MainMap } from '~/constants/map'; +import { mapZoomChange } from './map/actions'; const userPersistConfig: PersistConfig = { key: 'user', @@ -73,3 +75,4 @@ history.listen((location, action) => { }); watchLocation((location: LatLngLiteral) => store.dispatch(setUserLocation(location))); +MainMap.on('zoomend', event => store.dispatch(mapZoomChange(event.target._zoom))) diff --git a/src/styles/stickers.less b/src/styles/stickers.less index ef3dace..315524d 100644 --- a/src/styles/stickers.less +++ b/src/styles/stickers.less @@ -109,6 +109,11 @@ } } +.sticker-wrapper { + will-change: transform; + transition: transform 50ms; +} + .sticker-arrow { position: absolute; transform-origin: 0 0; diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 9235ed2..22cfdc2 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -103,9 +103,7 @@ export const getTilePlacement = (): ITilePlacement => { }; export const getPolyPlacement = (latlngs: LatLng[]): Point[] => - latlngs.length === 0 - ? [] - : latlngs.map(latlng => (MainMap.latLngToContainerPoint(latlng))); + latlngs.length === 0 ? [] : latlngs.map(latlng => MainMap.latLngToContainerPoint(latlng)); export const getStickersPlacement = (stickers: IStickerDump[]): IStickerPlacement[] => stickers.length === 0 @@ -132,7 +130,7 @@ export const imageFetcher = (source: string): Promise => export const fetchImages = ( ctx: CanvasRenderingContext2D, geometry: ITilePlacement, - provider, + provider ): Promise<{ x: number; y: number; image: HTMLImageElement }[]> => { const { minX, maxX, minY, maxY, zoom } = geometry; @@ -144,7 +142,11 @@ export const fetchImages = ( } return Promise.all( - images.map(({ x, y, source }) => imageFetcher(source).then(image => ({ x, y, image }))) + images.map(({ x, y, source }) => + imageFetcher(source) + .then(image => ({ x, y, image })) + .catch() + ) ); }; @@ -206,20 +208,19 @@ export const composePoly = ({ } if (color === 'gradient') { - const gradient = ctx.createLinearGradient(minX, minY, minX, maxY); gradient.addColorStop(0, COLORS.PATH_COLOR[0]); gradient.addColorStop(1, COLORS.PATH_COLOR[1]); - + ctx.strokeStyle = gradient; } else { ctx.strokeStyle = color; } if (dash) { - ctx.setLineDash([12,12]); + ctx.setLineDash([12, 12]); } - + ctx.stroke(); ctx.closePath(); }; @@ -292,10 +293,12 @@ const composeStickerArrow = ( ctx: CanvasRenderingContext2D, x: number, y: number, - angle: number + angle: number, + scale: number ) => { ctx.save(); ctx.translate(x, y); + ctx.scale(scale, scale); ctx.rotate(angle + Math.PI * 0.75); ctx.translate(-x, -y); ctx.fillStyle = '#ff3344'; @@ -309,12 +312,12 @@ const composeStickerArrow = ( ctx.restore(); }; -const measureRect = (x: number, y: number, width: number, height: number, reversed: boolean) => ({ - rectX: reversed ? x - width - 36 - 10 : x, - rectY: y - 7 - height / 2, - rectW: width + 36 + 10, - rectH: height + 20, - textX: reversed ? x - width - 36 : x + 36, +const measureRect = (x: number, y: number, width: number, height: number, scale: number, reversed: boolean) => ({ + rectX: reversed ? x - width - 36 * scale - 10 * scale : x, + rectY: y - 7 * scale - height / 2, + rectW: width + 46 * scale, + rectH: height + 20 * scale, + textX: reversed ? x - width - 36 : x + 36 * scale, }); const measureDistRect = ( @@ -374,39 +377,32 @@ const composeStickerText = ( x: number, y: number, angle: number, - text: string + text: string, + scale: number ): void => { const rad = 56; - const tX = Math.cos(angle + Math.PI) * rad - 30 + x + 28; - const tY = Math.sin(angle + Math.PI) * rad - 30 + y + 29; + const tX = (Math.cos(angle + Math.PI) * rad - 30 + 28) * scale + x; + const tY = (Math.sin(angle + Math.PI) * rad - 30 + 29) * scale + y; - ctx.font = '12px "Helvetica Neue", Arial'; + ctx.font = `${12 * scale}px "Helvetica Neue", Arial`; const lines = text.split('\n'); - const { width, height } = measureText(ctx, text, 16); + const { width, height } = measureText(ctx, text, 16 * scale); const { rectX, rectY, rectW, rectH, textX } = measureRect( tX, tY, width, height, + scale, angle > -(Math.PI / 2) && angle < Math.PI / 2 ); - // rectangle - // ctx.fillStyle = '#222222'; - // ctx.beginPath(); - // ctx.rect( - // rectX, - // rectY, - // rectW, - // rectH, - // ); - // ctx.closePath(); - // ctx.fill(); + roundedRect(ctx, rectX, rectY, rectW, rectH, '#222222', 2); // text ctx.fillStyle = 'white'; - lines.map((line, i) => ctx.fillText(line, textX, rectY + 6 + 16 * (i + 1))); + lines.map((line, i) => ctx.fillText(line, textX, rectY + (6 + 16 * (i + 1)) * scale)); + // ctx.scale(1/scale, 1/scale); }; export const composeDistMark = ({ @@ -451,36 +447,43 @@ const composeStickerImage = async ( y: number, angle: number, set: string, - sticker: string + sticker: string, + scale: number ): Promise => { + console.log({ scale }); + const rad = 56; - const tX = Math.cos(angle + Math.PI) * rad - 30 + (x - 8); - const tY = Math.sin(angle + Math.PI) * rad - 30 + (y - 4); + const tX = (Math.cos(angle + Math.PI) * rad - 30 - 6) * scale + x; + const tY = (Math.sin(angle + Math.PI) * rad - 30 - 6) * scale + y; const offsetX = STICKERS[set].layers[sticker].off * 72; - return imageFetcher(STICKERS[set].url).then(image => - ctx.drawImage(image, offsetX, 0, 72, 72, tX, tY, 72, 72) - ); + await imageFetcher(STICKERS[set].url).then(image => { + ctx.drawImage(image, offsetX, 0, 72, 72, tX, tY, 72 * scale, 72 * scale); + }); + + return; }; export const composeStickers = async ({ stickers, ctx, + zoom, }: { stickers: IStickerPlacement[]; ctx: CanvasRenderingContext2D; + zoom: number; }): Promise => { if (!stickers || stickers.length < 0) return; stickers.map(({ x, y, angle, text }) => { - composeStickerArrow(ctx, x, y, angle); + composeStickerArrow(ctx, x, y, angle, zoom); - if (text) composeStickerText(ctx, x, y, angle, text); + if (text) composeStickerText(ctx, x, y, angle, text, zoom); }); await Promise.all( stickers.map(({ x, y, angle, set, sticker }) => - composeStickerImage(ctx, x, y, angle, set, sticker) + composeStickerImage(ctx, x, y, angle, set, sticker, zoom) ) ); };