diff --git a/package-lock.json b/package-lock.json index 1488464..f5626f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3630,6 +3630,11 @@ "sha.js": "^2.4.8" } }, + "croppr": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/croppr/-/croppr-2.3.1.tgz", + "integrity": "sha512-0rvTl4VmR3I4AahjJPF1u9IlT7ckvjIcgaLnUjYaY+UZsP9oxlVYZWYDuqM3SVCQiaI7DXMjR7wOEYT+mydOFg==" + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -5234,6 +5239,11 @@ "schema-utils": "^0.4.5" } }, + "file-saver": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz", + "integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", diff --git a/package.json b/package.json index 48de546..a042617 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "babel-runtime": "^6.26.0", "classnames": "^2.2.6", "clean-webpack-plugin": "^0.1.9", + "croppr": "^2.3.1", + "file-saver": "^2.0.0", "history": "^4.7.2", "leaflet": "^1.3.4", "leaflet-editable": "^1.1.0", diff --git a/src/components/panels/EditorPanel.jsx b/src/components/panels/EditorPanel.jsx index 6cf7ebb..f073392 100644 --- a/src/components/panels/EditorPanel.jsx +++ b/src/components/panels/EditorPanel.jsx @@ -9,10 +9,8 @@ import { EditorDialog } from '$components/panels/EditorDialog'; import { LogoPreview } from '$components/logo/LogoPreview'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { setMode, startEditing, stopEditing, setLogo, showRenderer } from '$redux/user/actions'; +import { setMode, startEditing, stopEditing, setLogo, takeAShot } from '$redux/user/actions'; import type { UserType } from '$constants/types'; -import { editor } from '$modules/Editor'; -import { getTilePlacement } from '$utils/renderer'; type Props = { user: UserType, @@ -31,7 +29,7 @@ type Props = { startEditing: Function, stopEditing: Function, setLogo: Function, - showRenderer: Function, + takeAShot: Function, } class Component extends React.PureComponent { @@ -39,8 +37,6 @@ class Component extends React.PureComponent { const obj = document.getElementById('control-dialog'); const { width } = this.panel.getBoundingClientRect(); - console.log(obj, this.panel); - if (!this.panel || !obj) return; obj.style.width = width; @@ -109,7 +105,7 @@ class Component extends React.PureComponent { + + +
+ +
+ + + +
+
); } } + +const mapStateToProps = state => ({ ...state.user.renderer, logo: state.user.logo }); + +const mapDispatchToProps = dispatch => bindActionCreators({ + hideRenderer, + cropAShot, +}, dispatch); + +export const Renderer = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/index.html b/src/index.html index 14b4756..28543d9 100644 --- a/src/index.html +++ b/src/index.html @@ -26,9 +26,16 @@ padding: 0; margin: 0; } + + canvas#renderer { + position: fixed; + left: 0; + top: 0; + } +
diff --git a/src/index.js b/src/index.js index 952990a..94f5cec 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,14 @@ /* + todo shot mechanism (50%) + done client-side shot mechanism + todo croppr.js + todo shot stickers + todo hotkeys via sagas - todo shot mechanism - todo crop mechanism todo map catalogue + todo map preview on save todo tooltips - todo client-side shot mechanism */ import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/src/redux/user/actions.js b/src/redux/user/actions.js index 2460942..7d5df87 100644 --- a/src/redux/user/actions.js +++ b/src/redux/user/actions.js @@ -34,3 +34,7 @@ export const setSaveOverwrite = () => ({ type: ACTIONS.SET_SAVE_OVERWRITE }); export const showRenderer = () => ({ type: ACTIONS.SHOW_RENDERER }); export const hideRenderer = () => ({ type: ACTIONS.HIDE_RENDERER }); +export const setRenderer = payload => ({ type: ACTIONS.SET_RENDERER, payload }); +export const takeAShot = () => ({ type: ACTIONS.TAKE_A_SHOT }); +export const cropAShot = payload => ({ type: ACTIONS.CROP_A_SHOT, ...payload }); + diff --git a/src/redux/user/constants.js b/src/redux/user/constants.js index 9180b5c..26b4304 100644 --- a/src/redux/user/constants.js +++ b/src/redux/user/constants.js @@ -33,4 +33,7 @@ export const ACTIONS = { SHOW_RENDERER: 'SHOW_RENDERER', HIDE_RENDERER: 'HIDE_RENDERER', + SET_RENDERER: 'SET_RENDERER', + TAKE_A_SHOT: 'TAKE_A_SHOT', + CROP_A_SHOT: 'CROP_A_SHOT', }; diff --git a/src/redux/user/reducer.js b/src/redux/user/reducer.js index 7702c56..65e44ae 100644 --- a/src/redux/user/reducer.js +++ b/src/redux/user/reducer.js @@ -65,6 +65,11 @@ const hideRenderer = state => ({ renderer: { ...state.renderer, renderer_active: false } }); +const setRenderer = (state, { payload }) => ({ + ...state, + renderer: { ...state.renderer, ...payload } +}); + const HANDLERS = { [ACTIONS.SET_USER]: setUser, [ACTIONS.SET_EDITING]: setEditing, @@ -85,6 +90,7 @@ const HANDLERS = { [ACTIONS.SHOW_RENDERER]: showRenderer, [ACTIONS.HIDE_RENDERER]: hideRenderer, + [ACTIONS.SET_RENDERER]: setRenderer, }; export const INITIAL_STATE = { diff --git a/src/redux/user/sagas.js b/src/redux/user/sagas.js index d7b09e2..8d10f7d 100644 --- a/src/redux/user/sagas.js +++ b/src/redux/user/sagas.js @@ -3,10 +3,11 @@ import { delay } from 'redux-saga'; import { takeLatest, select, call, put, takeEvery, race, take } from 'redux-saga/effects'; import { checkUserToken, getGuestToken, getStoredMap, postMap } from '$utils/api'; import { + hideRenderer, setActiveSticker, setAddress, setChanged, setEditing, - setMode, + setMode, setRenderer, setSaveError, setSaveOverwrite, setSaveSuccess, setTitle, setUser @@ -17,6 +18,15 @@ import { ACTIONS } from '$redux/user/constants'; import { MODES } from '$constants/modes'; import { DEFAULT_USER } from '$constants/auth'; import { TIPS } from '$constants/tips'; +import { + composeImages, + composePoly, downloadCanvas, + fetchImages, + getPolyPlacement, + getTilePlacement, + imageFetcher +} from '$utils/renderer'; +import { LOGOS } from '$constants/logos'; const getUser = state => (state.user.user); const getState = state => (state.user); @@ -217,6 +227,61 @@ function* setSaveSuccessSaga({ address, title }) { return yield editor.setInitialData(); } +function* getRenderData() { + const canvas = document.getElementById('renderer'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + const ctx = canvas.getContext('2d'); + + const geometry = getTilePlacement(); + const points = getPolyPlacement(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const images = yield fetchImages(ctx, geometry); + yield composeImages({ geometry, images, ctx }); + yield composePoly({ points, ctx }); + + return yield canvas.toDataURL('image/jpeg'); +} + +function* takeAShotSaga() { + const data = yield call(getRenderData); + + yield put(setRenderer({ + data, renderer_active: true, width: window.innerWidth, height: window.innerHeight + })); + + return true; +} + + +function* getCropData({ + x, y, width, height +}) { + const { logo, renderer: { data } } = yield select(getState); + const canvas = document.getElementById('renderer'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const image = yield imageFetcher(data); + const logoImage = yield imageFetcher(LOGOS[logo][1]); + + ctx.drawImage(image, -x, -y); + ctx.drawImage(logoImage, width - logoImage.width, height - logoImage.height); + + return yield canvas.toDataURL('image/jpeg'); +} + +function* cropAShotSaga(params) { + const { title, address } = yield select(getState); + yield call(getCropData, params); + const canvas = document.getElementById('renderer'); + + downloadCanvas(canvas, (title || address)); + + return yield put(hideRenderer()); +} + export function* userSaga() { // ASYNCHRONOUS!!! :-) @@ -241,4 +306,6 @@ export function* userSaga() { yield takeLatest(ACTIONS.SEND_SAVE_REQUEST, sendSaveRequestSaga); yield takeLatest(ACTIONS.SET_SAVE_SUCCESS, setSaveSuccessSaga); + yield takeLatest(ACTIONS.TAKE_A_SHOT, takeAShotSaga); + yield takeLatest(ACTIONS.CROP_A_SHOT, cropAShotSaga); } diff --git a/src/sprites/icon.svg b/src/sprites/icon.svg index b4bbd22..d790bb5 100644 --- a/src/sprites/icon.svg +++ b/src/sprites/icon.svg @@ -293,8 +293,14 @@ + + + + + + - + diff --git a/src/styles/panel.less b/src/styles/panel.less index dae14fb..941785e 100644 --- a/src/styles/panel.less +++ b/src/styles/panel.less @@ -117,6 +117,11 @@ background-size: 100% 100%; } + &.success { + background: linear-gradient(150deg, @green_primary, @green_secondary) 50% 50% no-repeat; + background-size: 100% 100%; + } + &.danger { background: linear-gradient(150deg, @red_primary, @red_secondary) 50% 50% no-repeat; background-size: 100% 100%; diff --git a/src/styles/renderer.less b/src/styles/renderer.less index 74ad47d..50a7b62 100644 --- a/src/styles/renderer.less +++ b/src/styles/renderer.less @@ -5,11 +5,48 @@ width: 100%; height: 100%; z-index: 1000; + background: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 80px; + box-sizing: border-box; - background: rgba(0, 0, 0, 0.5); + > div { + display: flex; + align-items: center; + justify-content: center; + position: relative; + } - //canvas { - // width: 50vw; - // height: 50vh; - //} + img#rendererOutput { + width: 300px; + height: 300px; + } + + .croppr-region { + box-shadow: rgba(255, 255, 255, 0.2) 0 0 0 6px, rgba(0, 0, 0, 0.3) 0 0 0 1px; + border: none; + position: absolute; + top: 0; + left: 0; + overflow: hidden; + } + + .croppr-handle { + width: 12px; + height: 12px; + border-radius: 8px; + border: none; + box-shadow: rgba(0, 0, 0, 0.3) 0 0 0 1px; + } +} + +.renderer-logo { + position: absolute; + bottom: 0; + right: 0; + pointer-events: none; + + transform-origin: 100% 100%; } diff --git a/src/utils/renderer.js b/src/utils/renderer.js index 67c694f..a3d96e6 100644 --- a/src/utils/renderer.js +++ b/src/utils/renderer.js @@ -1,10 +1,9 @@ import { editor } from '$modules/Editor'; import { COLORS, CONFIG } from '$config'; - -const { map } = editor.map; -map.addEventListener('mousedown', ({ latlng }) => console.log('CLICK', latlng)); +import saveAs from 'file-saver'; const latLngToTile = latlng => { + const { map } = editor.map; const zoom = map.getZoom(); const xtile = parseInt(Math.floor((latlng.lng + 180) / 360 * (1 << zoom))); const ytile = parseInt(Math.floor((1 - Math.log(Math.tan(latlng.lat * Math.PI / 180) + 1 / Math.cos(latlng.lat * Math.PI / 180)) / Math.PI) / 2 * (1 << zoom))); @@ -13,6 +12,7 @@ const latLngToTile = latlng => { }; const tileToLatLng = point => { + const { map } = editor.map; const z = map.getZoom(); const lng = (point.x / Math.pow(2, z) * 360 - 180); const n = Math.PI - 2 * Math.PI * point.y / Math.pow(2, z); @@ -22,6 +22,7 @@ const tileToLatLng = point => { }; export const getTilePlacement = () => { + const { map } = editor.map; const width = window.innerWidth; const height = window.innerHeight; @@ -60,12 +61,12 @@ export const getTilePlacement = () => { export const getPolyPlacement = () => ( (!editor.poly.poly || !editor.poly.poly.getLatLngs() || editor.poly.poly.getLatLngs().length <= 0) ? [] - : editor.poly.poly.getLatLngs().map((latlng) => ({ ...map.latLngToContainerPoint(latlng) })) + : editor.poly.poly.getLatLngs().map((latlng) => ({ ...editor.map.map.latLngToContainerPoint(latlng) })) ); const getImageSource = ({ x, y, zoom }) => (`http://b.basemaps.cartocdn.com/light_all/${zoom}/${x}/${y}.png`); -const imageFetcher = source => new Promise((resolve, reject) => { +export const imageFetcher = source => new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); @@ -140,3 +141,6 @@ export const composePoly = ({ points, ctx }) => { return true; }; + +export const downloadCanvas = (canvas, title) => canvas.toBlob(blob => saveAs(blob, title)); +