前言
最近在重製自己的網頁,為了有更好的展示空間,所以也順便做了文章的縮圖編輯器。本系統基於 React 框架,但大部分是由 Js 實現,輕鬆移植到不同平台。
使用componentDidMount
React 生命週期中, componentDidMount 是在 render() 結束後呼叫,可用於建立該元件的基本參數,在此用來呼叫抓取 DOM 物件 (因為在 render 之前該 DOM 物件並不存在)。
const openEditor = () => { var canvas = document.getElementById("avatarCanvas"), context = canvas.getContext("2d"); } class AvatarEditor extends React.Component { state = {}; constructor(props) { super(props); this.state = this.props; } componentDidMount() { openEditor(); } render() { return ( <div> <canvas id="avatarCanvas" width="500" height="500" className="avatarEditor-canvas" ></canvas> </div> ); } }
設定 React Component 預設值
在上面我們創了一個繼承自 React.Component 的類別 AvatarEditor 。
設定該元件的預設值可使用類別 .defaultProps :
AvatarEditor.defaultProps = { img: "https://react.semantic-ui.com/images/wireframe/image.png", scale: 100, width: 100, height: 100, top: 0, left: 0, //preview大小 previewSize: 300, onsubmit: (e) => { console.log("submit"); }, oncancel:(e)=>{ //取消 } };
畫四個角的控制器
import React from "react"; import "../Css/AvatarEditor.css"; const openEditor = () => { var canvas = document.getElementById("avatarCanvas"), context = canvas.getContext("2d"), width = canvas.width, height = canvas.height; //因為rect是左上為中心開始畫 var controllerSize = 25; var rectSize = 500 * (1 - controllerSize / width); //四個角落 var points = [ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }, ]; renderPoints(); function update() {} function renderPoints() { context.clearRect(0, 0, width, height); for (var i = 0; i < points.length; i++) { var p = points[i]; context.fillStyle = "#464646"; context.beginPath(); context.rect(p.x * rectSize, p.y * rectSize, 25, 25); context.fill(); } } console.log(canvas); }; class AvatarEditor extends React.Component { state = {}; constructor(props) { super(props); this.state = this.props; } componentDidMount() { openEditor(); } render() { return ( <div> <canvas id="avatarCanvas" width="500" height="500" className="avatarEditor-canvas" ></canvas> </div> ); } } export default AvatarEditor;
拖拉控制
透過控制對角線的x與y的位置,一同移動其餘的三個控制點。
var controllerSize = 25; var controllerSizeRatio = controllerSize / width; var rectSize = 500 * (1 - controllerSizeRatio); //四個角落 var points = [ { x: 0, y: 1 }, { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, ]; var isDraggingController = false; //右下控制器 var sizeController = document.getElementById("sizeController"); sizeController.onmousedown = function (e) { e.preventDefault(); isDraggingController = true; }; document.body.addEventListener("mousemove", function (event) { //移動size controller位置 if (isDraggingController) { var mousePos = getMousePos(canvas, event); var moveRate = 1 - mousePos.y / rectSize; sizeController.style.right = moveRate * 100 + "%"; sizeController.style.bottom = moveRate * 100 + "%"; //修改其他點位置: points[0].y = 1 - controllerSizeRatio - moveRate; points[2].x = 1 - controllerSizeRatio - moveRate; } }); .... render() { return ( <div className="avatarEditor-root"> <canvas id="avatarCanvas" width="500" height="500" className="avatarEditor-canvas" ></canvas> <button id="sizeController" className="avatarEditor-controller" onClick={(e) => { e.preventDefault(); }} > 123 </button> </div> ); }
拖拉位置
透過改變與父物件相對位置的 top , left 值實現移動的效果。
//位置控制器 var positionController = document.getElementById("positionController"); positionController.onmousedown = function (e) { e.preventDefault(); is_gragging_reposisiton_controller = true; previous_mouse_pos=getMousePos(canvas,e); }; //位置控制器--原本mouse位置 var previous_mouse_pos = { x: 0, y: 0 }; document.body.addEventListener("mousemove", function (event) { //改變size if (is_gragging_resize_controller) { var mousePos = getMousePos(canvas, event); var moveRate = 1 - mousePos.y / rectSize; sizeController.style.right = moveRate * 100 + "%"; sizeController.style.bottom = moveRate * 100 + "%"; //修改其他點位置: points[0].y = 1 - controllerSizeRatio - moveRate; points[2].x = 1 - controllerSizeRatio - moveRate; points[3].x = 1 - controllerSizeRatio - moveRate; points[3].y = 1 - controllerSizeRatio - moveRate; //修改選擇範圍 positionController.style.height = (points[0].y - points[1].y) * 100 + "%"; positionController.style.width = (points[2].x - points[0].x) * 100 + "%"; positionController.style.top = points[1].y * 100 + "%"; positionController.style.left = points[0].x * 100 + "%"; } //移動選取位置 else if (is_gragging_reposisiton_controller) { var mousePos = getMousePos(canvas, event); //offset轉成%數 var offsetX = (mousePos.x - previous_mouse_pos.x) / rectMaxSize; var offsetY = (mousePos.y - previous_mouse_pos.y) / rectMaxSize; //移動每個points for (var i = 0; i < points.length; i++) { var p = points[i]; p.x += offsetX; p.y += offsetY; console.log(p); points[i] = p; } //移動resize按鈕 sizeController.style.right = (1 - points[3].x - controllerSizeRatio) * 100 + "%"; sizeController.style.bottom = (1 - points[3].y - controllerSizeRatio) * 100 + "%"; //移動inner範圍 positionController.style.top = points[1].y * 100 + "%"; positionController.style.left = points[0].x * 100 + "%"; previous_mouse_pos = mousePos; } });
同步操作縮圖位置
同樣在操作預覽圖片的 top , left 值達到效果。
{/* 預覽 */} <div className="avatarEditor-preview-container" id="preview" style={{ height: this.state.previewSize, width: this.state.previewSize }} > <img src={this.state.img} id="avatarEditor-preview-img"></img> </div> function updatePreviewImg() { var previewWidthRatio = 1 / (points[3].x - points[1].x); previewImg.style.width = previewWidthRatio * 100 + "%"; previewImg.style.height = previewWidthRatio * 100 + "%"; previewImg.style.left = -points[0].x * previewWidthRatio * 100 + "%"; previewImg.style.top = -points[1].y * previewWidthRatio * 100 + "%"; previewResult = previewImg; }
如何使用?
先宣告全域變數previewResult 乘載編輯結果。
var previewResult;
並透過按鈕呼叫本元件的 callback 方法。
handleSubmit(e) { //用點的位子計算縮放比例: var scale = (1 / (this.state.points[3].x - this.state.points[1].x)) * 100 + "%"; var data = { //e: e, scale: scale, src: this.state.img, width: e.width, height: e.height, top: e.style.top, left: e.style.left, }; this.state.onsubmit(data); }
還記得我們在設定元件初始值的時候宣告了 onsubmit 方法當作傳入參數。在執行建構子時將該方法參數連接本程式的方法。
注意等號左邊的是傳入的 callback 方法參數( property ),右邊的是本程式碼的方法。
constructor(props) { super(props); this.state = this.props; this.handleSubmit = this.handleSubmit.bind(this); this.handleURLChange = this.handleURLChange.bind(this); }
至此,本元件腳本結束。
外部呼叫
呼叫開啟編輯器:
將 useState() 方法當作參數傳入,使在元件裡面和下確認後能更新資料給外部元件。
const [thumbnail, setThumbnail] = useState({ src: "https://react.semantic-ui.com/images/wireframe/image.png", }); .... <div style={{ display: openAvatarEditor ? "block" : "none" }}> <AvatarEditor img={thumbnail.src} scale={thumbnail.scale} width={thumbnail.scale} height={thumbnail.scale} top={thumbnail.top} left={thumbnail.left} onsubmit={(e) => { setThumbnail(e); setOpenAvatarEditor(false); }} oncancel={(e) => { setOpenAvatarEditor(false); }} ></AvatarEditor> </div>
透過更新圖片大小、上下錨點呈現效果。
💡 注意圖片大小是紀錄縮放百分比例而不是固定的px數值,如此在不同大小的物件下才能正常顯示。 (例如:編輯頁面的預覽圖片大小是300X300px,但在小屋清單上是200X200px,若紀錄的是圖片的px數值,則會發生跑版)
<img src={thumbnail.src} className="createArticle__image" style={{ width: thumbnail.scale, height: thumbnail.scale, top: thumbnail.top, left: thumbnail.left, }} ></img>