目錄
- 緣由-胡扯
- 本文實驗環境
- 通用流程
- 1.基礎移動
- 1.1 基礎代碼
- 1.1.1 data-* 解釋
- 1.2 操作元素創建
- 1.3 css 修飾
- 1.4 cdn 引入
- 1.5 js 實現元素可移動
- 1.6 圖片拖拽
- 2.縮放
- 3.旋轉
- 4.裁剪
懶得改文案了,海報編輯器換方案了,如果后面用別的再更。
緣由-胡扯
導火索:睡不著,現在是 25/1/25 快早上了
在這段時間突然有了一個新項目,該項目與前端通過 img 素材編輯一個海報有相同的需求點,又或者說該需求是弱于海報編輯的。
最開始,我是打算直接通過 js 直接實現對應的功能,但是總有一些小bug,剛好公司的前端推給我了 moveable
,瞟了一眼后,發現需求完美符合,moveable
可實現前端對單個、多個元素的拖拽、組合、編輯、縮放、旋轉、拉扯等操作,甚至于有一種“殺雞焉用牛刀之感”。
但隨著項目的迭代,未來的需求不可得知,但用發展的眼光看待這個項目的話,直接實現一個海報編輯器是最優的選擇。
接下來我把我總結的 moveable
編寫為教程,包括海報編輯器的制作寫在該文之中,計劃上下兩篇,畢竟一篇使用基礎,另一篇就是編輯器的制作。
想必,各位也不想在二次開發的時候看不懂 moveable
的實現邏輯吧,能平滑過渡就平滑過渡吧,大家的腦子都不想承載過多的計算,張飛:俺也一樣。
本文實驗環境
系統:Windows
前端:html
框架:無
js:其實我不是前端,就當是原生吧,因為筆者并不熟悉標準
編輯器:vs code
參考示例:https://daybrush.com/moveable/storybook/index.html
GitHub:https://github.com/daybrush/moveable
apidoc:https://daybrush.com/moveable/release/latest/doc/
通用流程
首先在此通過 vs 編寫一個基礎的 html (快捷鍵感嘆號會自動彈出,選擇單感嘆號即可):
此時選擇之后將會創建一個基礎的 html 基礎代碼(如果沒有的就直接復制吧):
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body></body>
</html>
1.基礎移動
1.1 基礎代碼
我們首先實現 moveable 對元素的基礎移動。
要實現 moveable 的基礎移動需要創建一個 div
為其根容器,此時我們在 body
元素下創建一個 div
,并給與樣式修飾 class="root"
,此處的樣式修飾并不是必須項,在此只是為了更好的進行演示,當然,在 storybook 上給與的官方示例也是如此。
此時在基礎的 html 元素中的 body
下,創建一個 div
為根節點,代碼如下:
<div class="root"></div>
那么此時根節點有了,接下來就需要創建 moveable 的容器。你聽的沒錯,這是一個比較通用的概念,實現某個特殊的元素時,使用一個容器作為存儲是一個非常常見的方式。
此時在根節點下,創建一個 div
為容器:
<div class="root"><div class="container" data-croffle-ref="element$0"></div>
</div>
在以上的 html 代碼中,樣式修飾為 class="container"
則是容器的元素節點,但可能你對這個節點的疑問在 data-croffle-ref="element$0"
,如果沒疑問就更好了,在此給一些不理解這個元素屬性的讀者做一下解釋。
1.1.1 data-* 解釋
在以上代碼中 data-croffle-ref 為 html5 引入的一種機制,這個機制簡單的來說是讓開發者在不影響本身元素的語義的情況下,為當前元素增加新的元數據,又或者說開發者自定義了當前元素的屬性。
一般 data-*
自定義元數據可以與 document.querySelector
搭配,準確的找到對應的元數據,接下來在編寫 js 代碼時將會解釋這一部分。
在這里,讀者可以理解為此時定義了一個 moveable 的容器,并且自定義了一個屬性 croffle-ref
,但由于自定義屬性的編寫方式為帶前綴 data-
,即編寫為 data-croffle-ref="element$0"
,其中 element$0
為這個自定義屬性的值。
1.2 操作元素創建
當創建完操作元素的容器后,接下來創建用于控制的元素,則創建一個拖動、縮放、選擇等操作的元素。
畢竟我們的目標就是添加不同的圖片到當前編輯器,并對這些圖片進行拖拽、旋轉、縮放等操作。
此時在容器下添加一個 div 用于存放對應的操作元素,此時 body 下的html 代碼如下:
<div class="root"><div class="container" data-croffle-ref="element$0"><div class="target" data-croffle-ref="targetRef">Target</div></div>
</div>
以上代碼中,樣式修飾為 class="target"
的 div 元素則為我們的操作元素,并且這個元素由于 moveable
的機制,給與了 data-croffle-ref="targetRef"
的自定義屬性,并且這個 div 的內容為 Target
文本。
1.3 css 修飾
其實也不需要修飾,但是沒有樣式的話可能讀者會覺得很奇怪,讀者可以將 css 刪除查看效果,最后發現還是加上 css 的為好(示例摘抄于 storybook 但做了精簡和增加了便于文章講解的額外內容)。
css 直接復制在 head 上即可,在此處已經給與了 style 標簽:
<style>
.root {position: relative;
}.container {position: relative;margin-top: 50px;
}.target {position: absolute;width: 100px;height: 100px;top: 150px;left: 100px;line-height: 100px;text-align: center;background: #ee8;color: #333;font-weight: bold;border: 1px solid #333;box-sizing: border-box;
}
</style>
以上標簽中,給與了 root 根元素與容器 root 的定位方式為相對定位,接著給與了操作元素 target 的樣式為一個黃色的矩形框。
css 的話我不做多的解釋,其實這是布局設計問題,咱們若不是前端就直接看詳細的功能實現即可,若是前端頁不用說對吧,如果不懂的直接復制即可,畢竟這是個樣式,沒有設計到特效動效制作。
接下來 cdn 引入后會給出這一部分的代碼:
1.4 cdn 引入
以下是 moveable 的 cdn:
<script src="https://cdnjs.cloudflare.com/ajax/libs/moveable/0.53.0/moveable.min.js"integrity="sha512-gFIuV9WCEJeWYkY1ZdJXugypot9ooEtwJf6U8In5JR6z5ZvV1xAvAQe9mQ7IYBXiF9ICXyiCeqgCJzqf64wh7A=="crossorigin="anonymous" referrerpolicy="no-referrer"></script>
如果 cdn 引入有問題,可以在 https://cdnjs.com/libraries/moveable 查看。
此處我也給予 moveable.min.js
的下載地址:TODO
此時的代碼(除 js 外應該是這樣):
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>csdn 1_bit moveable how to use</title><style>.root {position: relative;}.container {position: relative;margin-top: 50px;}.target {position: absolute;width: 100px;height: 100px;top: 150px;left: 100px;line-height: 100px;text-align: center;background: #ee8;color: #333;font-weight: bold;border: 1px solid #333;box-sizing: border-box;}</style>
</head><body><div class="root"><div class="container" data-croffle-ref="element$0"><div class="target" data-croffle-ref="targetRef">Target</div></div></div>
</body></html>
1.5 js 實現元素可移動
在 moveable 中,若想讓一個元素可移動其實很簡單,在此我先列出代碼:
const element$0 = document.querySelector(`[data-croffle-ref="element$0"]`);
const targetRef = document.querySelector(`[data-croffle-ref="targetRef"]`);const moveable$0 = new Moveable(element$0, {target: targetRef,draggable: true,throttleDrag: 1,edgeDraggable: false,startDragRotate: 0,throttleDragRotate: 0}
);moveable$0.on("drag", e => {e.target.style.transform = e.transform;
});
首先我們看最開始的兩行,這兩行都是通過 document.querySelector 尋找元素,其中一個元素是[data-croffle-ref="element$0"]
為容器元素,另一個為 [data-croffle-ref="targetRef"]
為操作元素。
容器元素與操作元素的獲取這兩者是必不可少的,通過確定這倆者從而進行控制。
之前也說過 document.querySelector
與自定義元素搭配即可精確找到元素,在此列出代碼后不再贅述了。
在找到元素后,直接 new 一個 Moveable,其中第一個參數傳入的是在 html 中定義的 element$0
;第二個參數則是操作參數,則你需要如何操作這個元素。
第二個參數為一個字典,在字典中指定了操作的元素 target
為 targetRef
,當前全部 key 鍵的解釋如下:
target
操作的目標元素draggable
目標元素是否可拖動throttleDrag
拖動的延時毫秒數,設置為 0 則表示實時更新edgeDraggable
表示目標的邊緣是否可拖動startDragRotate
當鼠標旋轉多少后才使元素進行轉動throttleDragRotate
旋轉的延時毫秒數,設置為 0 則表示實時更新
你可能在思考,當前操作只是移動目標元素,并沒有進行旋轉等操作,為什么會有這些參數。
其實當前示例時摘抄于 storybook 之上,在此列出為方便 查看 storybook 演示的讀者作為解釋,當前示例并不需要那么多的參數,只需要傳入如下字典即可實現拖拽操作:
{target: targetRef,//目標元素draggable: true//是否可拖拽
}
那么最后的代碼:
moveable$0.on("drag", e => {e.target.style.transform = e.transform;
});
則表示為 Moveable 實例的 drag 事件添加一個事件監聽器,當目標元素被拖動時,會觸發這個事件監聽器;其中代碼為 e.target.style.transform = e.transform;
則表示事件對象 e 包含了拖動操作的相關信息,其中 e.transform 是拖動后的變換樣式,將 e.transform 的值賦給目標元素的 style.transform 屬性,更新目標元素的位置。
此時當前拖拽的所有代碼如下(省略了 style 樣式,直接復制在 1.3 中的 css 即可):
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>csdn 1_bit moveable how to use</title><script src="https://cdnjs.cloudflare.com/ajax/libs/moveable/0.53.0/moveable.min.js"integrity="sha512-gFIuV9WCEJeWYkY1ZdJXugypot9ooEtwJf6U8In5JR6z5ZvV1xAvAQe9mQ7IYBXiF9ICXyiCeqgCJzqf64wh7A=="crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head><body><div class="root"><div class="container" data-croffle-ref="element$0"><div class="target" data-croffle-ref="targetRef">Target</div></div></div><script>const element$0 = document.querySelector(`[data-croffle-ref="element$0"]`);const targetRef = document.querySelector(`[data-croffle-ref="targetRef"]`);const moveable$0 = new Moveable(element$0, {target: targetRef,draggable: true});moveable$0.on("drag", e => {e.target.style.transform = e.transform;});</script>
</body></html>
此時效果如下:
你可能會想,如果咱們可是使用的是圖片,并不是文字,不要擔心,圖片他來了。
1.6 圖片拖拽
此時在 html 中,操作元素是通過一個容器進行包裹的,既然是容器,那么是包裹整個操作元素的,那么此時我們只需要將操作元素下添加一個圖片即可。
例如在操作元素下添加一個 img 標簽:
<div class="target" data-croffle-ref="targetRef"><img src="1.jpg" alt="" style="width: 100px;height: 100px;">
</div>
此時我給與了這個 img 的樣式修飾 style="width: 100px;height: 100px;"
,此時的樣式修飾是為了滿足操作元素的大小,否則將會超出(雖然不會影響當前的功能,但美觀上接收不了,之后將會用更加“優雅”的方式解決這個問題)。
圖片各位自己放到自己的路徑下即可,在此不在贅述,此時的結果如下:
2.縮放
注意:以下 html 代碼與 第 1 小點 相同
使用 moveable 還可以對添加的圖片進行縮放,只需要 new 一個Moveable 時傳入不同的參數即可:
const moveable$0 = new Moveable(element$0, {target: targetRef,scalable: true,// 啟用縮放功能,設置為 true 表示允許對目標元素進行縮放操作keepRatio: false,// 將 keepRatio 變量的值賦給配置對象的 keepRatio 屬性,控制縮放時是否保持寬高比renderDirections: renderDirections
});
其中參數定義如下:
- scalable 啟用縮放功能,設置為 true 表示允許對目標元素進行縮放操作
- keepRatio 控制縮放時是否保持寬高比
- false為不保持 renderDirections 控制顯示縮放的控制點
其中 scalable 與 keepRatio 都比較好理解,只有 renderDirections 較為陌生。
renderDirections 是一個 list,這個list 存儲了要顯示控制點的內容,例如這個 renderDirections 的代碼為 const renderDirections = ["n", "s"];
則表示在北部和南部有一個控制點,即如下圖所示:
此時具體 js 代碼部分如下:
const renderDirections = ["n", "s"];
const element$0 = document.querySelector(`[data-croffle-ref="element$0"]`);
const targetRef = document.querySelector(`[data-croffle-ref="targetRef"]`);
const moveable$0 = new Moveable(element$0, {target: targetRef,scalable: true,// 啟用縮放功能,設置為 true 表示允許對目標元素進行縮放操作keepRatio: false,// 控制縮放時是否保持寬高比renderDirections: renderDirections
});moveable$0.on("scale", e => {e.target.style.transform = e.drag.transform;
});
其中代碼:
moveable$0.on("scale", e => {e.target.style.transform = e.drag.transform;
});
為表示監聽 scale 縮放事件,通過 e.drag.transform 得到最新的樣式信息給予到目標元素即可改變以及重新繪制。
此時還可以更改 renderDirections 為 const renderDirections = ["nw","n","ne","w","e","sw","s","se"];
,其中 nw 表示東南、ne 表示西南、以此類推運行代碼后展現如下:
其展示效果如下:
若將 keepRatio: false,// 控制縮放時是否保持寬高比
改成 true,效果如下:
3.旋轉
設定目標是否可進行旋轉也只是需要 new 一個 moveable 時傳入的參數即可:
const moveable$0 = new Moveable(element$0, {target: targetRef,rotatable: true,rotationPosition: rotationPosition
});
參數說明如下:
- rotatable 控制目標元素是否可旋轉 true 為允許
- rotationPosition 指定旋轉控制點的位置
以上代碼中 rotationPosition 設置為字符串類型的 val 為 “top” 即可,即:const rotationPosition = "top";
之后為 moveable$0 設置 rotate 事件監聽:
moveable$0.on("rotate", e => {e.target.style.transform = e.drag.transform;
});
那么即可設置目標元素的響應。
完整的 js 代碼如下:
const rotationPosition = "top";
const element$0 = document.querySelector(`[data-croffle-ref="element$0"]`);
const targetRef = document.querySelector(`[data-croffle-ref="targetRef"]`);
const moveable$0 = new Moveable(element$0, {target: targetRef,rotatable: true,// 控制目標元素是否可旋轉rotationPosition: rotationPosition// 指定旋轉控制點的位置
});
moveable$0.on("rotate", e => {e.target.style.transform = e.drag.transform;
});
此時的 html 代碼并不需要修改,此時頁面所展示的內容如下:
此時的控制點在頂部,若設置 rotationPosition 為 bottom const rotationPosition = "bottom";
時頁面展示效果如下:
此時的操作效果如下:
4.裁剪
在 moveable 中,一般情況下,只需要在 new moveable 時傳入不同的參數即可對操作目標開啟不同的操作,最后再為其添加對應操作的事件響應,即可完成對開啟的操作完成監聽。
以下是一個完成裁剪功能的 moveable 代碼:
const moveable$0 = new Moveable(element$0, {target: targetRef,draggable: true,clippable: true,clipTargetBounds: false,
});
以下為各個新增參數的解釋:
- draggable 控制目標元素是否可拖動
- clippable 控制目標元素是否可裁剪
- clipRelative 控制裁剪區域的定位方式
- clipTargetBounds 控制裁剪區域是否受目標元素邊界限制
此時讀者可能發現,以上再 new moveable 時允許目標可拖動以及目標可剪切,那我該怎么樣監聽兩個事件呢?其實很簡單,只需要使用 on 監聽兩個事件即可,即:
moveable$0.on("drag", e => {e.target.style.transform = e.transform;
});
moveable$0.on("clip", e => {e.target.style.clipPath = e.clipStyle;
});
再 drag 時給與 e.transform 到 e.target.style.transform 的 transform 即可,再 clip 時給與 e.clipStyle 到 e.target.style.clipPath 即可。
接下來我們繼續回到參數之中,其中 clipTargetBounds 表示在裁剪時會不會超出區域了還可以裁剪,當設置為 true 時不允許過邊界(藍框裁剪),結果如下:
如果設置為 false則允許過邊界,效果如下(也演示了拖拽效果):
此時完整的js 代碼如下(html 不需要進行修改):
const element$0 = document.querySelector(`[data-croffle-ref="element$0"]`);
const targetRef = document.querySelector(`[data-croffle-ref="targetRef"]`);const moveable$0 = new Moveable(element$0, {target: targetRef,draggable: true,clippable: true,clipTargetBounds: false,
});moveable$0.on("drag", e => {e.target.style.transform = e.transform;
});moveable$0.on("clip", e => {e.target.style.clipPath = e.clipStyle;
});