mirror of
https://github.com/voltbonn/profile-picture-generator.git
synced 2024-12-22 07:45:04 +00:00
added basic editing (moving + scaling)
it doesn't yet work on touch devinces
This commit is contained in:
parent
f70c295da3
commit
2e6795f79f
7 changed files with 421 additions and 57 deletions
|
@ -7,6 +7,8 @@
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"hamsterjs": "^1.1.3",
|
||||||
"merge-images": "^2.0.0",
|
"merge-images": "^2.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
|
7
public/hammer.min.js
vendored
Normal file
7
public/hammer.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/volt-logo-64.png" />
|
<link rel="icon" href="%PUBLIC_URL%/volt-logo-64.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
@ -24,6 +24,8 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<script src="./%PUBLIC_URL%/hammer.min.js"></script>
|
||||||
<title>Volt Social Media Frame Generator</title>
|
<title>Volt Social Media Frame Generator</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
30
src/App.css
30
src/App.css
|
@ -13,11 +13,14 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
background-color: var(--background);
|
/* min-height: 100vh; */
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -117,3 +120,26 @@ button:hover,
|
||||||
.droparea.active{
|
.droparea.active{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.Editor{
|
||||||
|
pointer-events: all;
|
||||||
|
position: relative;
|
||||||
|
margin: 2vh;
|
||||||
|
font-size: 0;
|
||||||
|
cursor: move;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.Editor .background{
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.Editor .foreground{
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
width: 36vh;
|
||||||
|
height: 36vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
209
src/App.js
209
src/App.js
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
import FrameChooser from './FrameChooser.js'
|
import FrameChooser from './FrameChooser.js'
|
||||||
|
import Editor from './Editor.js'
|
||||||
import HeaderImage from './HeaderImage.svg'
|
import HeaderImage from './HeaderImage.svg'
|
||||||
|
|
||||||
import mergeImages from 'merge-images'
|
import mergeImages from 'merge-images'
|
||||||
|
@ -53,16 +54,37 @@ function getOrientation(file, callback) {
|
||||||
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trigger_download(name, data){
|
||||||
|
const a = document.createElement('a')
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.target = '_blank'
|
||||||
|
a.download = name
|
||||||
|
a.href = data // "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAABWSURBVDhPY0xISPh//0UOA7mAiVyNMH2jBjAwkBQGjD9KGBTEJ6OEO0kG2NvbMwCjnXwDsEU5SS5ANuDhjRCGJbPFSQsDdBfIyMhQZgDIQLK9QLWkDABPsQw5I+5qmAAAAABJRU5ErkJggg==";
|
||||||
|
a.click()
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [frameURL, setFrameURL] = useState(null)
|
const [frameURL, setFrameURL] = useState(null)
|
||||||
const [originalPhoto, setOriginalPhoto] = useState(null)
|
const [originalPhoto, setOriginalPhoto] = useState(null)
|
||||||
const [photo, setPhoto] = useState(null)
|
const [originalPhotoRation, setOriginalPhotoRation] = useState(1)
|
||||||
const [combinedImage, setCombinedImage] = useState(null)
|
const [orientation, set_orientation] = useState(null)
|
||||||
|
|
||||||
|
|
||||||
|
// const [combinedImage, set_combinedImage] = useState(null)
|
||||||
|
|
||||||
|
const [width, set_width] = useState(0)
|
||||||
|
const [height, set_height] = useState(0)
|
||||||
|
|
||||||
|
const [cords, setCords] = useState({x:0, y:0, scale:1})
|
||||||
|
|
||||||
const handleFrameURL = useCallback(newFrameURL => {
|
const handleFrameURL = useCallback(newFrameURL => {
|
||||||
setFrameURL(newFrameURL)
|
setFrameURL(newFrameURL)
|
||||||
}, [setFrameURL])
|
}, [setFrameURL])
|
||||||
|
|
||||||
|
const handleCordsChange = useCallback(({x, y, scale}) => {
|
||||||
|
console.log({ x, y, scale })
|
||||||
|
setCords({ x, y, scale })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleReadFile = useCallback(file => {
|
const handleReadFile = useCallback(file => {
|
||||||
if (!(!!file)) {
|
if (!(!!file)) {
|
||||||
|
@ -73,12 +95,6 @@ function App() {
|
||||||
reader.onload = reader_event => {
|
reader.onload = reader_event => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.onload = function () {
|
img.onload = function () {
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = frameSize
|
|
||||||
canvas.height = frameSize
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d', { alpha: true })
|
|
||||||
|
|
||||||
let width, height;
|
let width, height;
|
||||||
if (img.width < img.height) {
|
if (img.width < img.height) {
|
||||||
height = (img.height / img.width) * frameSize
|
height = (img.height / img.width) * frameSize
|
||||||
|
@ -88,65 +104,53 @@ function App() {
|
||||||
width = (img.width / img.height) * frameSize
|
width = (img.width / img.height) * frameSize
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrientation(file, orientation => {
|
getOrientation(file, new_orientation => {
|
||||||
|
let original_ration = 1
|
||||||
// use the correct image orientation
|
// use the correct image orientation
|
||||||
switch (orientation) {
|
switch (new_orientation) {
|
||||||
// Source: https://stackoverflow.com/a/30242954/2387277
|
// Source: https://stackoverflow.com/a/30242954/2387277
|
||||||
// Source: https://stackoverflow.com/questions/19463126/how-to-draw-photo-with-correct-orientation-in-canvas-after-capture-photo-by-usin
|
// Source: https://stackoverflow.com/questions/19463126/how-to-draw-photo-with-correct-orientation-in-canvas-after-capture-photo-by-usin
|
||||||
case 2:
|
case 2:
|
||||||
// horizontal flip
|
// horizontal flip
|
||||||
ctx.translate(canvas.width, 0)
|
original_ration = height / width
|
||||||
ctx.scale(-1, 1)
|
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
// 180° rotate left
|
// 180° rotate left
|
||||||
ctx.translate(canvas.width, canvas.height)
|
original_ration = height / width
|
||||||
ctx.rotate(Math.PI)
|
|
||||||
break
|
break
|
||||||
case 4:
|
case 4:
|
||||||
// vertical flip
|
// vertical flip
|
||||||
ctx.translate(0, canvas.height)
|
original_ration = height / width
|
||||||
ctx.scale(1, -1)
|
|
||||||
break
|
break
|
||||||
case 5:
|
case 5:
|
||||||
// vertical flip + 90 rotate right
|
// vertical flip + 90 rotate right
|
||||||
ctx.rotate(0.5 * Math.PI)
|
original_ration = width / height
|
||||||
ctx.scale(1, -1)
|
|
||||||
break
|
break
|
||||||
case 6:
|
case 6:
|
||||||
// 90° rotate right
|
// 90° rotate right
|
||||||
ctx.rotate(0.5 * Math.PI)
|
original_ration = width / height
|
||||||
ctx.translate(0, -canvas.height)
|
|
||||||
break
|
break
|
||||||
case 7:
|
case 7:
|
||||||
// horizontal flip + 90 rotate right
|
// horizontal flip + 90 rotate right
|
||||||
ctx.rotate(0.5 * Math.PI)
|
original_ration = width / height
|
||||||
ctx.translate(canvas.width, -canvas.height)
|
|
||||||
ctx.scale(-1, 1)
|
|
||||||
break
|
break
|
||||||
case 8:
|
case 8:
|
||||||
// 90° rotate left
|
// 90° rotate left
|
||||||
ctx.rotate(-0.5 * Math.PI)
|
original_ration = width / height
|
||||||
ctx.translate(-canvas.width, 0)
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
original_ration = height / width
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(
|
set_width(width)
|
||||||
img,
|
set_height(height)
|
||||||
(frameSize - width) / 2,
|
setOriginalPhoto(reader_event.target.result)
|
||||||
(frameSize - height) / 2,
|
set_orientation(new_orientation)
|
||||||
width,
|
setOriginalPhotoRation(original_ration)
|
||||||
height,
|
|
||||||
)
|
|
||||||
|
|
||||||
const pngUrl = canvas.toDataURL()
|
|
||||||
setPhoto(pngUrl)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
img.src = reader_event.target.result
|
img.src = reader_event.target.result
|
||||||
setOriginalPhoto(reader_event.target.result)
|
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -159,6 +163,102 @@ function App() {
|
||||||
handleReadFile(acceptedFiles[0])
|
handleReadFile(acceptedFiles[0])
|
||||||
}, [handleReadFile])
|
}, [handleReadFile])
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = frameSize
|
||||||
|
canvas.height = frameSize
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d', { alpha: true })
|
||||||
|
|
||||||
|
// use the correct image orientation
|
||||||
|
switch (orientation) {
|
||||||
|
// Source: https://stackoverflow.com/a/30242954/2387277
|
||||||
|
// Source: https://stackoverflow.com/questions/19463126/how-to-draw-photo-with-correct-orientation-in-canvas-after-capture-photo-by-usin
|
||||||
|
case 2:
|
||||||
|
// horizontal flip
|
||||||
|
ctx.translate(canvas.width, 0)
|
||||||
|
ctx.scale(-1, 1)
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
// 180° rotate left
|
||||||
|
ctx.translate(canvas.width, canvas.height)
|
||||||
|
ctx.rotate(Math.PI)
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
// vertical flip
|
||||||
|
ctx.translate(0, canvas.height)
|
||||||
|
ctx.scale(1, -1)
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
// vertical flip + 90 rotate right
|
||||||
|
ctx.rotate(0.5 * Math.PI)
|
||||||
|
ctx.scale(1, -1)
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
// 90° rotate right
|
||||||
|
ctx.rotate(0.5 * Math.PI)
|
||||||
|
ctx.translate(0, -canvas.height)
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
// horizontal flip + 90 rotate right
|
||||||
|
ctx.rotate(0.5 * Math.PI)
|
||||||
|
ctx.translate(canvas.width, -canvas.height)
|
||||||
|
ctx.scale(-1, 1)
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
// 90° rotate left
|
||||||
|
ctx.rotate(-0.5 * Math.PI)
|
||||||
|
ctx.translate(-canvas.width, 0)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const width_scaled = width * cords.scale
|
||||||
|
const height_scaled = height * cords.scale
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
cords.x * 3.5 + (frameSize - width_scaled) * 0.5,
|
||||||
|
cords.y * 3.5 + (frameSize - height_scaled) * 0.5,
|
||||||
|
width_scaled,
|
||||||
|
height_scaled,
|
||||||
|
)
|
||||||
|
// ctx.drawImage(
|
||||||
|
// img,
|
||||||
|
// ((frameSize - width_scaled) * 0.5),
|
||||||
|
// ((frameSize - height_scaled) * 0.5),
|
||||||
|
// width_scaled,
|
||||||
|
// height_scaled,
|
||||||
|
// )
|
||||||
|
|
||||||
|
const pngUrl = canvas.toDataURL()
|
||||||
|
|
||||||
|
mergeImages([
|
||||||
|
...(pngUrl ? [pngUrl] : []),
|
||||||
|
...(frameURL ? [frameURL] : []),
|
||||||
|
])
|
||||||
|
.then(b64 => {
|
||||||
|
// set_combinedImage(b64)
|
||||||
|
trigger_download('volt-profile-picture.png', b64)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
img.src = originalPhoto
|
||||||
|
}, [
|
||||||
|
originalPhoto,
|
||||||
|
cords.x,
|
||||||
|
cords.y,
|
||||||
|
cords.scale,
|
||||||
|
orientation,
|
||||||
|
frameURL,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
])
|
||||||
|
|
||||||
const { isDragActive, getRootProps } = useDropzone({
|
const { isDragActive, getRootProps } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: 'image/*',
|
accept: 'image/*',
|
||||||
|
@ -167,15 +267,6 @@ function App() {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mergeImages([
|
|
||||||
...(photo ? [photo] : []),
|
|
||||||
...(frameURL ? [frameURL] : []),
|
|
||||||
])
|
|
||||||
.then(b64 => setCombinedImage(b64))
|
|
||||||
}, [photo, frameURL])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App" {...getRootProps()}>
|
<div className="App" {...getRootProps()}>
|
||||||
<img src={HeaderImage} className="HeaderImage" alt="Volt Logo" />
|
<img src={HeaderImage} className="HeaderImage" alt="Volt Logo" />
|
||||||
|
@ -188,18 +279,28 @@ function App() {
|
||||||
<p>It should best be a square image or your face in the middle. The photo is not saved and never leaves your computer.</p>
|
<p>It should best be a square image or your face in the middle. The photo is not saved and never leaves your computer.</p>
|
||||||
|
|
||||||
<label className="labelButton" tabIndex="0" style={{outline:'none'}}>
|
<label className="labelButton" tabIndex="0" style={{outline:'none'}}>
|
||||||
{!!photo ? <img src={originalPhoto} alt="Preview" /> : null}
|
{!!originalPhoto ? <img src={originalPhoto} alt="Preview" /> : null}
|
||||||
<span>{!!photo ? 'Change Photo' : 'Load Photo'}</span>
|
<span>{!!originalPhoto ? 'Change Photo' : 'Load Photo'}</span>
|
||||||
<input onChange={handleImage} type="file" accept="image/*" style={{display: 'none'}} />
|
<input onChange={handleImage} type="file" accept="image/*" style={{display: 'none'}} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<FrameChooser onFrameChange={handleFrameURL} />
|
{!!originalPhoto ? (<>
|
||||||
|
<FrameChooser onFrameChange={handleFrameURL} />
|
||||||
|
</>) : null}
|
||||||
|
|
||||||
<h2>Download your Photo:</h2>
|
{!!originalPhoto && !!frameURL ? (<>
|
||||||
<img src={combinedImage} className="FinishedFrame" alt="Finished Frame" />
|
<h2>Edit your Photo:</h2>
|
||||||
<a download="volt-profile-picture.png" href={combinedImage} target="_blank" rel="noreferrer">
|
<p>Your can reposition the image and scale it. Use pinch-to-zoom or scroll to scale.</p>
|
||||||
<button>Download Profile Picture</button>
|
|
||||||
</a>
|
<Editor
|
||||||
|
background={originalPhoto}
|
||||||
|
backgroundRatio={originalPhotoRation}
|
||||||
|
foreground={frameURL}
|
||||||
|
onChange={handleCordsChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button onClick={handleDownload}>Download Profile Picture</button>
|
||||||
|
</>) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
216
src/Editor.js
Normal file
216
src/Editor.js
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
|
||||||
|
import Hammer from 'hammerjs'
|
||||||
|
import Hamster from 'hamsterjs'
|
||||||
|
|
||||||
|
function updateRange(imageWidth, imageHeight, imageScale, containerWidth, containerHeight) {
|
||||||
|
|
||||||
|
const rangeX = Math.max(0, (imageWidth * imageScale) - containerWidth)
|
||||||
|
const rangeY = Math.max(0, (imageHeight * imageScale) - containerHeight)
|
||||||
|
|
||||||
|
const rangeMaxX = (rangeX / 2)
|
||||||
|
const rangeMinX = 0 - rangeMaxX
|
||||||
|
|
||||||
|
const rangeMaxY = (rangeY / 2)
|
||||||
|
const rangeMinY = 0 - rangeMaxY
|
||||||
|
|
||||||
|
return {
|
||||||
|
rangeMaxX,
|
||||||
|
rangeMinX,
|
||||||
|
rangeMaxY,
|
||||||
|
rangeMinY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.min(Math.max(min, value), max)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let minScale = 1;
|
||||||
|
let maxScale = 8;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function Editor({ onChange, background, backgroundRatio, foreground }) {
|
||||||
|
const editorRef = useRef(null)
|
||||||
|
const backgroundImageRef = useRef(null)
|
||||||
|
|
||||||
|
const [hammer_got_init, set_hammer_got_init] = useState(false)
|
||||||
|
|
||||||
|
const [hammertime, set_hammertime] = useState(null)
|
||||||
|
const [hamster, set_hamster] = useState(null)
|
||||||
|
|
||||||
|
const [x, set_x] = useState(0)
|
||||||
|
const [y, set_y] = useState(0)
|
||||||
|
const [add_x, set_add_x] = useState(0)
|
||||||
|
const [add_y, set_add_y] = useState(0)
|
||||||
|
const [scale, set_scale] = useState(1)
|
||||||
|
// const [add_scale, set_add_scale] = useState(0)
|
||||||
|
|
||||||
|
const [photoWidth, setPhotoWidth] = useState(300)
|
||||||
|
const [photoHeight, setPhotoHeight] = useState(300)
|
||||||
|
const [editorWidth, setEditorWidth] = useState(300)
|
||||||
|
const [editorHeight, setEditorHeight] = useState(300)
|
||||||
|
|
||||||
|
const [rangeMinX, set_rangeMinX] = useState(0)
|
||||||
|
const [rangeMinY, set_rangeMinY] = useState(0)
|
||||||
|
const [rangeMaxX, set_rangeMaxX] = useState(0)
|
||||||
|
const [rangeMaxY, set_rangeMaxY] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!onChange) {
|
||||||
|
onChange({ x, y, scale})
|
||||||
|
}
|
||||||
|
}, [onChange, x, y, scale])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!editorRef && !!editorRef.current) {
|
||||||
|
const new_editorWidth = editorRef.current.offsetWidth
|
||||||
|
const new_editorHeight = editorRef.current.offsetHeight
|
||||||
|
setEditorHeight(new_editorHeight)
|
||||||
|
setEditorWidth(new_editorWidth)
|
||||||
|
|
||||||
|
let new_photoWidth = 1
|
||||||
|
let new_photoHeight = 1
|
||||||
|
if (backgroundRatio < 1) {
|
||||||
|
new_photoWidth = 1 / backgroundRatio
|
||||||
|
} else if (backgroundRatio > 1) {
|
||||||
|
new_photoHeight = 1 * backgroundRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
setPhotoWidth(new_photoWidth)
|
||||||
|
setPhotoHeight(new_photoHeight)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
backgroundRatio,
|
||||||
|
foreground,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const {
|
||||||
|
rangeMinX,
|
||||||
|
rangeMinY,
|
||||||
|
rangeMaxX,
|
||||||
|
rangeMaxY,
|
||||||
|
} = updateRange(photoWidth * editorWidth, photoHeight * editorHeight, scale, editorWidth, editorHeight)
|
||||||
|
|
||||||
|
set_rangeMinX(rangeMinX)
|
||||||
|
set_rangeMinY(rangeMinY)
|
||||||
|
set_rangeMaxX(rangeMaxX)
|
||||||
|
set_rangeMaxY(rangeMaxY)
|
||||||
|
}, [
|
||||||
|
photoWidth,
|
||||||
|
photoHeight,
|
||||||
|
editorWidth,
|
||||||
|
editorHeight,
|
||||||
|
scale,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
set_x(0)
|
||||||
|
set_y(0)
|
||||||
|
set_add_x(0)
|
||||||
|
set_add_y(0)
|
||||||
|
set_scale(1)
|
||||||
|
}, [background])
|
||||||
|
|
||||||
|
const handleMove = useCallback(event => {
|
||||||
|
const prev_x = event.target.dataset.x * 1
|
||||||
|
const prev_y = event.target.dataset.y * 1
|
||||||
|
|
||||||
|
const new_x = clamp(prev_x + event.deltaX, rangeMinX, rangeMaxX)
|
||||||
|
const new_y = clamp(prev_y + event.deltaY, rangeMinY, rangeMaxY)
|
||||||
|
|
||||||
|
if (event.isFinal) {
|
||||||
|
set_x(new_x || 0)
|
||||||
|
set_y(new_y || 0)
|
||||||
|
set_add_x(0)
|
||||||
|
set_add_y(0)
|
||||||
|
}else{
|
||||||
|
set_add_x(new_x - prev_x || 0)
|
||||||
|
set_add_y(new_y - prev_y || 0)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
rangeMinX,
|
||||||
|
rangeMinY,
|
||||||
|
rangeMaxX,
|
||||||
|
rangeMaxY,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleScale = useCallback((event, delta, deltaX, deltaY) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const prev_scale = event.target.dataset.scale * 1
|
||||||
|
const new_scale = clamp(prev_scale + delta / 200, minScale, maxScale)
|
||||||
|
set_scale(new_scale || 1)
|
||||||
|
|
||||||
|
const prev_x = event.target.dataset.x * 1
|
||||||
|
const prev_y = event.target.dataset.y * 1
|
||||||
|
set_x(clamp(prev_x, rangeMinX, rangeMaxX) || 0)
|
||||||
|
set_y(clamp(prev_y, rangeMinY, rangeMaxY) || 0)
|
||||||
|
}, [
|
||||||
|
rangeMinX,
|
||||||
|
rangeMinY,
|
||||||
|
rangeMaxX,
|
||||||
|
rangeMaxY,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hammer_got_init && !!editorRef && !!editorRef.current) {
|
||||||
|
const element = editorRef.current
|
||||||
|
|
||||||
|
element.addEventListener('mousedown', e => e.preventDefault(), false)
|
||||||
|
|
||||||
|
set_hammertime(new Hammer(element, {
|
||||||
|
direction: 'DIRECTION_ALL',
|
||||||
|
}))
|
||||||
|
|
||||||
|
set_hamster(Hamster(element))
|
||||||
|
|
||||||
|
set_hammer_got_init(true)
|
||||||
|
}
|
||||||
|
}, [editorRef, hammer_got_init])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!hammertime && !!hamster && hammer_got_init && !!editorRef && !!editorRef.current) {
|
||||||
|
hammertime.on('pan', handleMove)
|
||||||
|
hamster.wheel(handleScale)
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
hammertime.off('pan', handleMove)
|
||||||
|
hamster.unwheel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editorRef, handleMove, handleScale, hammer_got_init, hammertime, hamster])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="Editor"
|
||||||
|
ref={editorRef}
|
||||||
|
data-x={x}
|
||||||
|
data-y={y}
|
||||||
|
data-scale={scale}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={background}
|
||||||
|
ref={backgroundImageRef}
|
||||||
|
alt=""
|
||||||
|
className="background"
|
||||||
|
style={{
|
||||||
|
width: (photoWidth*100)+'%',
|
||||||
|
height: (photoHeight*100)+'%',
|
||||||
|
transform: `translate3d(calc(-50% + ${x + add_x}px), calc(-50% + ${y + add_y}px), 0) scale(${scale},${scale})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={foreground}
|
||||||
|
alt=""
|
||||||
|
className="foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Editor
|
10
yarn.lock
10
yarn.lock
|
@ -5305,6 +5305,16 @@ gzip-size@5.1.1:
|
||||||
duplexer "^0.1.1"
|
duplexer "^0.1.1"
|
||||||
pify "^4.0.1"
|
pify "^4.0.1"
|
||||||
|
|
||||||
|
hammerjs@^2.0.8:
|
||||||
|
version "2.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
|
||||||
|
integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
|
||||||
|
|
||||||
|
hamsterjs@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/hamsterjs/-/hamsterjs-1.1.3.tgz#4adc6b9100a3fcc7c65fc391debb249901a50df6"
|
||||||
|
integrity sha512-q4XBr7hnxx1WyZA8mpVDuZVa1YXaR0WZaFSBxnj8hUXltuqXJOt5yuWYkAbMXsj+q0REDUO990+/TuxEadXFyg==
|
||||||
|
|
||||||
handle-thing@^2.0.0:
|
handle-thing@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||||
|
|
Loading…
Reference in a new issue