render: cropper & download image

This commit is contained in:
muerwre 2018-11-28 17:57:56 +07:00
parent 857a2a0c12
commit 34d1b85513
14 changed files with 308 additions and 40 deletions

10
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<Props, void> {
@ -39,8 +37,6 @@ class Component extends React.PureComponent<Props, void> {
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<Props, void> {
<button
className={classnames('disabled', { active: mode === MODES.SHOTTER })}
onClick={this.props.showRenderer}
onClick={this.props.takeAShot}
// onClick={getTilePlacement}
>
<Icon icon="icon-shot-3" />
@ -221,7 +217,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
setLogo,
startEditing,
stopEditing,
showRenderer,
takeAShot,
}, dispatch);
export const EditorPanel = connect(

View file

@ -1,30 +1,148 @@
import React from 'react';
import { getPolyPlacement, getTilePlacement, composeImages, composePoly, fetchImages } from '$utils/renderer';
export class Renderer extends React.Component {
componentDidMount() {
if (this.canvas) this.init();
import { hideRenderer, cropAShot } from '$redux/user/actions';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Croppr from 'croppr';
import 'croppr/dist/croppr.css';
import { Icon } from '$components/panels/Icon';
import { LOGOS } from '$constants/logos';
type Props = {
data: String,
logo: String,
hideRenderer: Function,
cropAShot: Function,
};
type State = {
};
class Component extends React.Component<Props, State> {
onImageLoaded = () => {
this.croppr = new Croppr(this.image, {
onCropMove: this.moveLogo,
onInitialize: this.onCropInit,
});
};
componentWillUnmount() {
this.croppr.destroy();
}
init() {
const ctx = this.canvas.getContext('2d');
const geometry = getTilePlacement();
const points = getPolyPlacement();
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
onCropInit = (crop) => {
fetchImages(ctx, geometry)
.then(images => composeImages({ geometry, images, ctx }))
.then(() => composePoly({ points, ctx }))
.then(() => this.canvas.toDataURL('image/jpeg'));
// .then(image => window.open().document.write(`<img src="${image}" />`))
}
const { regionEl, box } = crop;
const scale = ((box.x2 - box.x1) / window.innerWidth);
console.log('CROP', crop, scale);
this.logo = document.createElement('div');
this.logo.className = 'renderer-logo';
this.logo.style.transform = `scale(${scale})`;
this.logoImg = document.createElement('img');
this.logoImg.src = LOGOS[this.props.logo][1];
this.logo.append(this.logoImg);
regionEl.append(this.logo);
};
moveLogo = ({ x, y, width, height }) => {
if (!this.logo) return;
this.logo.style.color = 'blue';
};
croppr;
getImage = () => {
this.props.cropAShot(this.croppr.getValue());
};
render() {
const { data } = this.props;
const { innerWidth, innerHeight } = window;
const padding = 30;
const paddingBottom = 80;
let width;
let height;
// if (innerWidth > innerHeight) {
height = innerHeight - padding - paddingBottom;
width = height * (innerWidth / innerHeight);
// }
return (
<div className="renderer-shade" onClick={this.props.onClick}>
<canvas width={window.innerWidth} height={window.innerHeight} ref={el => { this.canvas = el; }} />
<div>
<div
className="renderer-shade"
style={{
padding,
paddingBottom,
}}
>
<div
style={{ width, height }}
>
<img
src={data}
alt=""
id="rendererOutput"
ref={el => { this.image = el; }}
onLoad={this.onImageLoaded}
/>
</div>
</div>
<div
className="panel active"
style={{
zIndex: 1000,
left: '50%',
right: 'auto',
transform: 'translateX(-50%)',
}}
>
<div className="control-bar control-bar-padded">
<button>
<Icon icon="icon-logo-3" />
</button>
</div>
<div className="control-sep" />
<div className="control-bar">
<button
className="highlighted cancel"
onClick={this.props.hideRenderer}
>
<Icon icon="icon-cancel-1" />
</button>
<button
className="success"
onClick={this.getImage}
>
<span>СКАЧАТЬ</span>
<Icon icon="icon-get-1" />
</button>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => ({ ...state.user.renderer, logo: state.user.logo });
const mapDispatchToProps = dispatch => bindActionCreators({
hideRenderer,
cropAShot,
}, dispatch);
export const Renderer = connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -26,9 +26,16 @@
padding: 0;
margin: 0;
}
canvas#renderer {
position: fixed;
left: 0;
top: 0;
}
</style>
</head>
<body>
<canvas id="renderer"></canvas>
<section id="map" style="position: absolute; width: 100%; height: 100%;"></section>
<section id="loader"></section>
<section id="index"></section>

View file

@ -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';

View file

@ -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 });

View file

@ -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',
};

View file

@ -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 = {

View file

@ -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);
}

View file

@ -293,8 +293,14 @@
<path xmlns="http://www.w3.org/2000/svg" d="M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2zm0 0c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z" fill="white" stroke="white" stroke-width="1" transform="translate(4 4)"/>
</g>
<g id="icon-get-1" stroke="none">
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black"/>
<path xmlns="http://www.w3.org/2000/svg" d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z" fill="white" stroke="white" stroke-width="1" transform="translate(4 4)"/>
</g>
</svg>
</defs>
<use xlink:href="#icon-shot-3" />
<use xlink:href="#icon-get-1" />
</svg>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -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%;

View file

@ -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%;
}

View file

@ -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));