JS Photo Scissor 手写一个图片裁剪器
Hello! In this article, we're gonna build a lightweight and handy photo cropper named Photo Scissor. Before we get started, hope you have a quick review of these tools:
- CSS transform and transform-origin
- Canvas
Alright! Let's get started!
Test environment
Google Chrome Version 86.0.4240.183
Create a file named photo-scissor.js and write the constructor function:
function PhotoScissor(element) {
this.element = element
}
And we're gonna create the infrastructure of our widget:
// style.js
//http://jsperf.com/vanilla-css
export function css(el, styles, val) {
if (typeof (styles) === "string") {
let tmp = styles
styles = {}
styles[tmp] = val
}
for (let prop in styles) {
el.style[prop] = styles[prop]
}
}
export function addClass(el, c) {
if (el.classList) {
el.classList.add(c)
}
else {
el.className += " " + c
}
}
export function removeClass(el, c) {
if (el.classList) {
el.classList.remove(c)
}
else {
el.className = el.className.replace(c, "")
}
}
// photo-scissor.js
function _create() {
this.data = {}
this.elements = {}
const boundary = this.elements.boundary = document.createElement("div")
const viewport = this.elements.viewport = document.createElement("div")
const img = this.elements.img = document.createElement("img")
const overlay = this.elements.overlay = document.createElement("div")
// we'll perform lots of operations on this element
this.elements.preview = img
addClass(boundary, "sc-boundary")
addClass(viewport, "sc-viewport")
addClass(this.elements.preview, "sc-image")
addClass(overlay, "sc-overlay")
this.element.appendChild(boundary)
boundary.appendChild(img)
boundary.appendChild(viewport)
boundary.appendChild(overlay)
addClass(this.element, "scissor-container")
}
It's pretty easy to understand what we are doing, the viewport will be the area that user want to crop, the overlay can make adding event listener much easier. Then we can use some CSS to make it light. Create a file named photo-scissor.css, then add the code, what worth noting is that we need to make the boundary relative to hold the absolute image.
.scissor-container {
width: 100%;
height: 100%;
}
.scissor-container .sc-boundary {
position: relative;
overflow: hidden;
margin: 0 auto;
z-index: 1;
width: 200px;
height: 200px;
}
.scissor-container .sc-image {
z-index: -1;
position: absolute;
max-height: none;
max-width: none;
}
.scissor-container .sc-viewport {
position: absolute;
border: 2px solid #fff;
margin: auto;
top: 0;
bottom: 0;
right: 0;
left: 0;
box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5);
z-index: 0;
width: 100px;
height: 100px;
}
.scissor-container .sc-overlay {
z-index: 1;
position: absolute;
cursor: move;
width: 200px;
height: 200px;
}
Fulfill the constructor function and make it useable:
function PhotoScissor(element, opts) {
this.element = element
this.options = opts
_create.call(this)
}
export default PhotoScissor
Next we can have a big step, create a demo.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Photo Scissor</title>
<link rel="stylesheet" href="photo-scissor.css"/>
<script type="module" src="photo-scissor.js"></script>
<style>
#demo {
width: 300px;
}
button {
margin: 10px 10px 10px 0;
}
</style>
</head>
<body>
<div id="demo"></div>
<button id="crop-button">Crop</button>
<div id="result"></div>
<script type="module">
import PhotoScissor from "./photo-scissor.js"
const demo = new PhotoScissor(document.getElementById("demo"), {
url: "medium.jpg",
})
</script>
</body>
</html>
demo.html
You may noticed that we have passed the photo URL to the scissor function, and let's make it work:
// photo-scissor.js
function PhotoScissor(element, opts) {
//...
if (this.options.url) {
_bind.call(this, {
url: this.options.url
})
}
}
function _bind(options) {
const url = options.url
this.data.url = url || this.data.url
loadImage(url).then(img => {
//
})
}
// image.js
export function loadImage(src) {
if (!src) { throw "Source image missing" }
const img = new Image()
img.style.opacity = "0"
return new Promise(function (resolve, reject) {
function _resolve() {
img.style.opacity = "1"
resolve(img)
}
img.onload = function () {
_resolve()
}
img.onerror = function (ev) {
img.style.opacity = 1
reject(ev)
}
img.src = src
})
}
It's quite standard image loading and while the image is loaded, we can replace the image into the widget boundary:
//...
loadImage(url).then(img => {
_replaceImage.call(this, img)
})
//...
function _replaceImage(img) {
if (this.elements.img.parentNode) {
// preserve the class
Array.prototype.forEach.call(this.elements.img.classList, (c) => {
addClass(img, c)
})
this.elements.img.parentNode.replaceChild(img, this.elements.img)
this.elements.preview = img // if the img is attached to the DOM, they're not using the canvas
}
this.elements.img = img
}
demo.html
It's pretty cool, right? but we can't let the image show like this, we should scale it smaller and center it to the viewport. That's the point we need to use the transform property, to make life easier we can write some helpers first:
// style.js
export const CSS_TRANSFORM = "transform"
export const CSS_TRANS_ORG = "transformOrigin"
export const CSS_USERSELECT = "userSelect"
export function Transform(x, y, scale) {
this.x = parseFloat(x)
this.y = parseFloat(y)
this.scale = parseFloat(scale)
}
Transform.parse = function (v) {
if (v.style) {
return Transform.parse(v.style[CSS_TRANSFORM])
}
else if (v.indexOf("matrix") > -1 || v.indexOf("none") > -1) {
return Transform.fromMatrix(v)
}
else {
return Transform.fromString(v)
}
}
Transform.fromMatrix = function (v) {
let vals = v.substring(7).split(",")
if (!vals.length || v === "none") {
vals = [1, 0, 0, 1, 0, 0]
}
return new Transform(num(vals[4]), num(vals[5]), parseFloat(vals[0]))
}
Transform.fromString = function (v) {
let transform
v.replace(/translate3d\((-?\d+(?:\.\d+)?)px, (-?\d+(?:\.\d+)?)px, (-?\d+(?:\.\d+)?)px\) scale\((\d+(?:\.\d+)?)\)/, (all, x, y, z, scale) => {
transform = new Transform(x, y, scale)
})
return transform
}
Transform.prototype.toString = function () {
return "translate3d" + "(" + this.x + "px, " + this.y + "px" + ", 0px" + ") scale(" + this.scale + ")"
}
export function TransformOrigin(el) {
if (!el || !el.style[CSS_TRANS_ORG]) {
this.x = 0
this.y = 0
return
}
const css = el.style[CSS_TRANS_ORG].split(" ")
this.x = parseFloat(css[0])
this.y = parseFloat(css[1])
}
TransformOrigin.prototype.toString = function () {
return this.x + "px " + this.y + "px"
}
Continue to write the loadImage resolve function:
loadImage(url).then(img => {
//...
_updatePropertiesFromImage.call(this)
})
function _updatePropertiesFromImage() {
let initialZoom = 1,
cssReset = {},
imgData,
transformReset = new Transform(0, 0, initialZoom)
cssReset[CSS_TRANSFORM] = transformReset.toString()
cssReset["opacity"] = 1
css(this.elements.preview, cssReset)
imgData = this.elements.preview.getBoundingClientRect()
this._originalImageWidth = imgData.width
this._originalImageHeight = imgData.height
}
After we reset the position of the image, we can perform the position manipulations:
// position.js
export function _centerImage() {
let imgDim = this.elements.preview.getBoundingClientRect(),
vpDim = this.elements.viewport.getBoundingClientRect(),
boundDim = this.elements.boundary.getBoundingClientRect(),
vpLeft = vpDim.left - boundDim.left,
vpTop = vpDim.top - boundDim.top,
w = (boundDim.width - imgDim.width) / 2,
h = (boundDim.height - imgDim.height) / 2,
transform = new Transform(w, h, this._currentZoom)
css(this.elements.preview, CSS_TRANSFORM, transform.toString())
}
demo.html
It's quite good that we can see the center of the image, but we still need to make it smaller, so here comes to the zoom function. First, we need to initialize the zoom:
// photo-scissor.js
function _create() {
//...
_initializeZoom.call(this)
}
// help.js
export function fix(v, decimalPoints) {
return parseFloat(v).toFixed(decimalPoints || 0)
}
export function num(v) {
return parseInt(v, 10)
}
// zoom.js
function _setZoomerVal(v) {
let z = this.elements.zoomer,
val = fix(v, 4)
z.value = Math.max(parseFloat(z.min), Math.min(parseFloat(z.max), val)).toString()
}
export function _initializeZoom() {
let wrap = this.elements.zoomerWrap = document.createElement("div"),
zoomer = this.elements.zoomer = document.createElement("input")
addClass(wrap, "sc-slider-wrap")
addClass(zoomer, "sc-slider")
zoomer.type = "range"
zoomer.step = "0.0001"
zoomer.value = "1"
zoomer.min = '0'
zoomer.max = '1.5'
this.element.appendChild(wrap)
wrap.appendChild(zoomer)
this._currentZoom = 1
function change() {
this._currentZoom = parseFloat(zoomer.value)
}
function scroll(event) {
let delta, targetZoom
if (event.wheelDelta) {
delta = event.wheelDelta / 1200 // wheelDelta min: -120 max: 120 // max x 10 x 2
} else if (event.deltaY) {
delta = event.deltaY / 1060 // deltaY min: -53 max: 53 // max x 10 x 2
} else if (event.detail) {
delta = event.detail / -60 // delta min: -3 max: 3 // max x 10 x 2
} else {
delta = 0
}
targetZoom = this._currentZoom + (delta * this._currentZoom)
event.preventDefault()
_setZoomerVal.call(this, targetZoom)
change.call(this)
}
this.elements.zoomer.addEventListener("change", change.bind(this))
this.elements.boundary.addEventListener("mousewheel", scroll.bind(this))
this.elements.boundary.addEventListener("DOMMouseScroll", scroll.bind(this))
}
Now if we refresh the page we can see a fresh slide bar, let's place it just below the cropper.
/* photo-scissor.css */
.scissor-container .sc-slider-wrap {
width: 75%;
margin: 15px auto;
text-align: center;
}
We'll find that the slide bar is now working smoothly except when it reaching the zero, in a real situation we will never want it to reach the zero, so let's update it based on the image dimension.
// photo-scissor.js
function _updatePropertiesFromImage() {
//...
_updateZoomLimits.call(this)
}
// help.js
export function dispatchChange(element) {
if ("createEvent" in document) {
const evt = document.createEvent("HTMLEvents")
evt.initEvent("change", false, true)
element.dispatchEvent(evt)
}
}
// zoom.js
export function _updateZoomLimits() {
let minZoom = 0,
maxZoom = 1.5,
initialZoom,
defaultInitialZoom,
zoomer = this.elements.zoomer,
scale = parseFloat(zoomer.value),
boundaryData = this.elements.boundary.getBoundingClientRect(),
imgData = naturalImageDimensions(this.elements.img),
vpData = this.elements.viewport.getBoundingClientRect(),
minW,
minH
// never let the photo smaller than the viewport either side
minW = vpData.width / imgData.width
minH = vpData.height / imgData.height
minZoom = Math.max(minW, minH)
zoomer.min = fix(minZoom, 4)
zoomer.max = fix(maxZoom, 4)
defaultInitialZoom = Math.max((boundaryData.width / imgData.width), (boundaryData.height / imgData.height))
_setZoomerVal.call(this, defaultInitialZoom)
dispatchChange(zoomer)
}
Now the slide bar is working even better, even the photo is not scaling. Next, let's wield our wands:
export function _initializeZoom() {
//...
function change() {
_onZoom.call(this, {
value: parseFloat(zoomer.value),
viewportRect: this.elements.viewport.getBoundingClientRect(),
transform: Transform.parse(this.elements.preview)
})
}
//...
}
function _onZoom(ui) {
let transform = ui ? ui.transform : Transform.parse(this.elements.preview),
vpRect = ui ? ui.viewportRect : this.elements.viewport.getBoundingClientRect(),
function applyCss() {
const transCss = {}
transCss[CSS_TRANSFORM] = transform.toString()
css(this.elements.preview, transCss)
}
this._currentZoom = ui ? ui.value : this._currentZoom
transform.scale = this._currentZoom
applyCss.apply(this)
}
demo.html
It's beautiful, right? We can scale it as we want now! Next, we need to add the drag function, that's pretty essential to the user experience.
export function _initDraggable() {
let isDragging = false,
originalX,
originalY,
originalDistance,
vpRect,
transform,
mouseMoveEv,
mouseUpEv
function assignTransformCoordinates(deltaX, deltaY) {
let previewRect = this.elements.preview.getBoundingClientRect(),
top = transform.y + deltaY,
left = transform.x + deltaX
if (vpRect.top > previewRect.top + deltaY && vpRect.bottom < previewRect.bottom + deltaY) {
transform.y = top
}
if (vpRect.left > previewRect.left + deltaX && vpRect.right < previewRect.right + deltaX) {
transform.x = left
}
}
function mouseDown(ev) {
if (ev.button !== undefined && ev.button !== 0) return
ev.preventDefault()
if (isDragging) return
isDragging = true
originalX = ev.pageX
originalY = ev.pageY
transform = Transform.parse(this.elements.preview)
window.addEventListener('mousemove', mouseMoveEv = mouseMove.bind(this))
window.addEventListener('mouseup', mouseUpEv = mouseUp.bind(this))
document.body.style[CSS_USERSELECT] = "none"
vpRect = this.elements.viewport.getBoundingClientRect()
}
function mouseMove(ev) {
ev.preventDefault()
let pageX = ev.pageX,
pageY = ev.pageY
let deltaX = pageX - originalX,
deltaY = pageY - originalY,
newCss = {}
assignTransformCoordinates.call(this, deltaX, deltaY)
newCss[CSS_TRANSFORM] = transform.toString()
css(this.elements.preview, newCss)
originalY = pageY
originalX = pageX
}
function mouseUp() {
isDragging = false
window.removeEventListener('mousemove', mouseMoveEv)
window.removeEventListener('mouseup', mouseUpEv)
document.body.style[CSS_USERSELECT] = ""
originalDistance = 0
}
this.elements.overlay.addEventListener('mousedown', mouseDown.bind(this))
}
Firstly we bind the mouse down event to the overlay element, the element that's born for it, and after started, we opt to bind another two event listener to the window so the user can drag more freely and safely.
demo.html
That's neat, but you may find that if we zoom after dragging, the photo seems like moving accidentally, and not scales using the center of the viewport as the center, the bigger the photo is, the bigger the problem is. That's the point we need to care about the transform-origin property.
The transform-origin CSS property sets the origin for an element's transformations. The transformation origin is the point around which a transformation is applied. For example, the transformation origin of the rotate() function is the center of rotation.
This property is applied by first translating the element by the value of the property, then applying the element's transform, then translating by the negated property value.
This means, this definition
transform-origin: -100% 50%;
transform: rotate(45deg);
results in the same transformation as
transform-origin: 0 0;
transform: translate(-100%, 50%) rotate(45deg) translate(100%, -50%);
By default, the origin of a transform is center. When we drag the center, the transform-origin of the preview does not coincide with the center of the viewport anymore. That's why need to change it dynamically.
Before that, let's look at another example:
transform-origin: 50px 50px;
transform: scale(0.5);
results in the same transformation as
transform-origin: 0 0;
transform: translate(50px, 50px) scale(0.5) translate(-50px, -50px);
In a real situation, how we combine the two translate functions into one?
x = y = 50px * (1 - scale)
(1 - scale) equals the blank bezel size after scaling. Then we have:
transform-origin: 0 0;
transform: translate(25px, 25px) scale(0.5);
The trick is at the scale function. If you don't understand it so well, try to make a demo to test it.
// photo-scissor.js
function _updatePropertiesFromImage() {
//...
let originReset = new TransformOrigin()
//...
cssReset[CSS_TRANSFORM] = transformReset.toString()
cssReset[CSS_TRANS_ORG] = originReset.toString()
cssReset["opacity"] = 1
css(this.elements.preview, cssReset)
}
Firstly, let's reset the origin in the first beginning and you may find the photo is missing if you refresh the page. Don't worry, let's write a update center origin function to correct it.
// photo-scissor.js
function _updatePropertiesFromImage() {
//...
_centerImage.call(this)
_updateCenterPoint.call(this)
_updateZoomLimits.call(this)
}
// position.js
export function _updateCenterPoint() {
let scale = this._currentZoom,
previewRect = this.elements.preview.getBoundingClientRect(),
vpRect = this.elements.viewport.getBoundingClientRect(),
transform = Transform.parse(this.elements.preview.style[CSS_TRANSFORM]),
previewOrigin = new TransformOrigin(this.elements.preview),
// get the distance between preview's left top corner to viewport's center point
// that's the point we need to anchor the image relative to the viewport
top = (vpRect.top - previewRect.top) + (vpRect.height / 2),
left = (vpRect.left - previewRect.left) + (vpRect.width / 2),
center = {},
adj = {}
center.y = top / scale
center.x = left / scale
// why we need to change the transform?
// First we need to move the center point of the image to the viewport center and then perform the scale
// that is how the browser works
// after we moved the translate origin, we need to keep it at the center of the viewport
// the distance is determined by the distance we moved multiplies the rest of the scale
// to make sure that translate() move the anchor point to the right position
// to erase the side effect of combining using translate-origin and scale
// we subtract the distance that changing origin brings
adj.y = (center.y - previewOrigin.y) * (1 - scale)
adj.x = (center.x - previewOrigin.x) * (1 - scale)
transform.x -= adj.x
transform.y -= adj.y
const newCss = {}
newCss[CSS_TRANS_ORG] = center.x + 'px ' + center.y + 'px'
newCss[CSS_TRANSFORM] = transform.toString()
css(this.elements.preview, newCss)
}
Now the photo is back to normal! Another crucial point is that we need to call it after we process the mouse up event.
export function _initDraggable() {
//...
function mouseUp() {
//...
_updateCenterPoint.call(this)
}
//...
}
Now we can find that, however we drag the image, the center point of zooming is always the center point of the viewport.
To this point, we're almost done, but if we check carefully, after we drag the photo to one side and zoom it smaller, the photo still will cross the viewport's boundary which is unacceptable, let's figure out how to solve it.
demo.htmlThe main idea to solve this problem is simple: while zooming, whenever a side of the photo reaches the viewport's boundary, we set the origin to that intersection's position subtracts half of the viewport's size, and divides by scale:
// zoom.js
export function _initializeZoom() {
//...
function change() {
_onZoom.call(this, {
value: parseFloat(zoomer.value),
viewportRect: this.elements.viewport.getBoundingClientRect(),
transform: Transform.parse(this.elements.preview),
origin: new TransformOrigin(this.elements.preview)
})
}
//...
}
function _onZoom(ui) {
//...
let origin = ui ? ui.origin : new TransformOrigin(this.elements.preview)
//...
let boundaries = _getVirtualBoundaries.call(this, vpRect),
transBoundaries = boundaries.translate,
oBoundaries = boundaries.origin
if (transform.x >= transBoundaries.maxX) {
origin.x = oBoundaries.minX
transform.x = transBoundaries.maxX
}
if (transform.x <= transBoundaries.minX) {
origin.x = oBoundaries.maxX
transform.x = transBoundaries.minX
}
if (transform.y >= transBoundaries.maxY) {
origin.y = oBoundaries.minY
transform.y = transBoundaries.maxY
}
if (transform.y <= transBoundaries.minY) {
origin.y = oBoundaries.maxY
transform.y = transBoundaries.minY
}
applyCss.apply(this)
}
// position.js
export function _getVirtualBoundaries(viewport) {
let scale = this._currentZoom,
vpWidth = viewport.width,
vpHeight = viewport.height,
centerFromBoundaryX = this.elements.boundary.clientWidth / 2,
centerFromBoundaryY = this.elements.boundary.clientHeight / 2,
imgRect = this.elements.preview.getBoundingClientRect(),
curImgWidth = imgRect.width,
curImgHeight = imgRect.height,
halfWidth = vpWidth / 2,
halfHeight = vpHeight / 2
const maxX = ((halfWidth / scale) - centerFromBoundaryX) * -1
const minX = maxX - ((curImgWidth * (1 / scale)) - (vpWidth * (1 / scale)))
const maxY = ((halfHeight / scale) - centerFromBoundaryY) * -1
const minY = maxY - ((curImgHeight * (1 / scale)) - (vpHeight * (1 / scale)))
const originMinX = (1 / scale) * halfWidth
const originMaxX = (curImgWidth * (1 / scale)) - originMinX
const originMinY = (1 / scale) * halfHeight
const originMaxY = (curImgHeight * (1 / scale)) - originMinY
return {
translate: {
maxX: maxX,
minX: minX,
maxY: maxY,
minY: minY
},
origin: {
maxX: originMaxX,
minX: originMinX,
maxY: originMaxY,
minY: originMinY
}
}
}
Here we are to the last step! Crop the photo and output it.
// result.js
const RESULT_DEFAULTS = {
type: "canvas",
format: "png",
quality: 1
},
RESULT_FORMATS = ["jpeg", "webp", "png"]
function _get() {
let imgData = this.elements.preview.getBoundingClientRect(),
vpData = this.elements.viewport.getBoundingClientRect(),
x1 = vpData.left - imgData.left,
y1 = vpData.top - imgData.top,
x2 = x1 + this.elements.viewport.offsetWidth,
y2 = y1 + this.elements.viewport.offsetHeight,
scale = this._currentZoom
if (scale === Infinity || isNaN(scale)) {
scale = 1
}
x1 = Math.max(0, x1 / scale)
y1 = Math.max(0, y1 / scale)
x2 = Math.max(0, x2 / scale)
y2 = Math.max(0, y2 / scale)
return {
points: [fix(x1), fix(y1), fix(x2), fix(y2)],
zoom: scale,
}
}
export function _result(options) {
let data = _get.call(this),
opts = Object.assign({}, RESULT_DEFAULTS, options),
resultType = (typeof (options) === "string" ? options : (opts.type || "base64")),
format = opts.format,
quality = opts.quality,
vpRect = this.elements.viewport.getBoundingClientRect(),
ratio = vpRect.width / vpRect.height
data.outputWidth = vpRect.width
data.outputHeight = vpRect.height
if (RESULT_FORMATS.indexOf(format) > -1) {
data.format = "image/" + format
data.quality = quality
}
data.url = this.data.url
return new Promise((resolve) => {
switch (resultType.toLowerCase()) {
default:
resolve(_getHtmlResult.call(this, data))
break
}
})
}
function _getHtmlResult(data) {
let points = data.points,
div = document.createElement("div"),
img = document.createElement("img"),
width = points[2] - points[0],
height = points[3] - points[1]
addClass(div, "scissor-result")
div.appendChild(img)
css(img, {
left: (-1 * points[0]) + "px",
top: (-1 * points[1]) + "px"
})
img.src = data.url
css(div, {
width: width + "px",
height: height + "px"
})
return div
}
Now, we default to output the Html format result, which is very convenient to let users preview what they will get.
<!--demo.html-->
<script type="module">
//...
// 彩蛋 bonus monkey-patching
if (typeof Element.prototype.clearChildren === "undefined") {
Object.defineProperty(Element.prototype, "clearChildren", {
configurable: true,
enumerable: false,
value: function() {
while(this.firstChild) this.removeChild(this.lastChild)
}
})
}
document.getElementById("crop-button").addEventListener("click", () => {
demo.result().then(result => {
const ele = document.getElementById("result")
ele.clearChildren()
ele.appendChild(result)
})
})
</script>
/*photo-scissor.css*/
.scissor-result {
position: relative;
overflow: hidden;
}
.scissor-result img {
position: absolute;
}
Html Result
Next, before we implement other types of result output, we need to do something with the canvas, a crucial tool can help us to crop the image and transform the image into a different format.
function _getCanvas(data) {
let points = data.points,
left = num(points[0]),
top = num(points[1]),
right = num(points[2]),
bottom = num(points[3]),
width = right - left,
height = bottom - top,
canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
startX = 0,
startY = 0,
canvasWidth = data.outputWidth || width,
canvasHeight = data.outputHeight || height
canvas.width = canvasWidth
canvas.height = canvasHeight
// By default assume we're going to draw the entire
// source image onto the destination canvas.
let sx = left,
sy = top,
sWidth = width,
sHeight = height,
dx = 0,
dy = 0,
dWidth = canvasWidth,
dHeight = canvasHeight
//
// Do not go outside of the original image's bounds along the x-axis.
// Handle translations when projecting onto the destination canvas.
//
// The smallest possible source x-position is 0.
if (left < 0) {
sx = 0
dx = (Math.abs(left) / width) * canvasWidth
}
// The largest possible source width is the original image's width.
if (sWidth + sx > this._originalImageWidth) {
sWidth = this._originalImageWidth - sx
dWidth = (sWidth / width) * canvasWidth
}
//
// Do not go outside of the original image's bounds along the y-axis.
//
// The smallest possible source y-position is 0.
if (top < 0) {
sy = 0
dy = (Math.abs(top) / height) * canvasHeight
}
// The largest possible source height is the original image's height.
if (sHeight + sy > this._originalImageHeight) {
sHeight = this._originalImageHeight - sy
dHeight = (sHeight / height) * canvasHeight
}
// console.table({ left, right, top, bottom, canvasWidth, canvasHeight, width, height, startX, startY, sx, sy, dx, dy, sWidth, sHeight, dWidth, dHeight })
ctx.drawImage(this.elements.preview, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
return canvas
}
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// result.js
export function _result(options) {
//...
return new Promise((resolve) => {
switch (resultType.toLowerCase()) {
case 'canvas':
resolve(_getCanvas.call(this, data))
break
case 'base64':
resolve(_getBase64Result.call(this, data))
break
case "blob":
_getBlobResult.call(this, data).then(resolve)
break
default:
resolve(_getHtmlResult.call(this, data))
break
}
})
}
function _getBase64Result(data) {
return _getCanvas.call(this, data).toDataURL(data.format, data.quality)
}
function _getBlobResult(data) {
return new Promise((resolve) => {
_getCanvas.call(this, data).toBlob((blob) => {
resolve(blob)
}, data.format, data.quality)
})
}
So that's it, in this article we built a fascinating photo scissor, user can use it to crop photos and get the various formats of output. We are using heavily of CSS properties like transform and transform-origin, and we depend on canvas to output the image part we want.