序文
最近、ユーザーのログイン認証にスライド画像コンポーネントを使用する必要があるプロジェクトがあり、それを使用するために github にアクセスし、プロジェクト開発に適したプラグインを見つけました。
- vue-スライダー-検証
しかし、学習の精神で、ソースコードを徹底的に研究し、優れたコードのアイデアを真似し、自分自身のレベルを向上させることにしました。
テクノロジーの選択
- ビュー 3
- キャンバス
コンポーネントの基本パラメータを決定する
実際にコンポーネントを書く過程では、どのようにすれば取引先にとって最も使いやすいかを考える必要がありますが、まず次のような利用方法を想定します。
typescript复制代码<vertify :height="80" :width="300" />
<script setup lang="ts">
import vertify from "@/components/Vertify/index.vue"
<script>
新しいindex.vueをcomponents/Vertifyに作成します。このコンポーネントは依然として従来のSFC形式で書かれています。
HTMLレイアウトを決定する
キャンバスに絵を描画できることを確認する方法
キャンバスを使用して画像を描画する場合は、描画する前に画像がロードされていることを確認する必要があります。そうしないと、画像がロードされない可能性があります。この問題を解決するには、JavaScript を使用して、描画前に画像が確実に読み込まれるようにすることができます。一般的な方法は、イメージの onload イベントを使用して、イメージのロード時に対応するコールバック関数をトリガーして描画操作を実行することです。
js复制代码<script setup lang="ts">
import { ref, onMounted } from "vue"
export interface IProps {
width?: number
height?: number
}
const props = withDefaults(defineProps<IProps>(), {
width: 320,
height: 160
})
//目标图片
const canvasRef = ref<HTMLCanvasElement | null>(null)
// 缺失的图片
const blockRef = ref<HTMLCanvasElement | null>(null)
const initImg = () => {
const img = createImg(() => {
draw(img)
})
}
function getRandomNumberByRange(start: number, end: number) {
return Math.round(Math.random() * (end - start) + start)
}
const getRandomImgSrc = () => {
return `https://picsum.photos/id/${getRandomNumberByRange(0, 1084)}/${props.width}/${props.height}`
}
const createImg = (onload: VoidFunction) => {
const img = new Image()
img.crossOrigin = "Anonymous"
img.onload = onload
img.onerror = () => {
;(img as any).setSrc(getRandomImgSrc()) // 图片加载失败的时候重新加载其他图片
}
;(img as any).setSrc = (src: string) => {
const isIE = window.navigator.userAgent.indexOf("Trident") > -1
if (isIE) {
// IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
const xhr = new XMLHttpRequest()
xhr.onloadend = function (e: any) {
const file = new FileReader() // FileReader仅支持IE10+
file.readAsDataURL(e.target.response)
file.onloadend = function (e) {
img.src = e?.target?.result as string
}
}
xhr.open("GET", src)
xhr.responseType = "blob"
xhr.send()
} else img.src = src
}
;(img as any).setSrc(getRandomImgSrc())
return img
}
const draw = (img: HTMLImageElement) => {
if (canvasRef.value && blockRef.value) {
const canvasCtx = canvasRef.value.getContext("2d") as CanvasRenderingContext2D
const blockCtx = blockRef.value.getContext("2d") as CanvasRenderingContext2D
// const img = createImg()
canvasCtx.drawImage(img, 0, 0, props.width, props.height)
}
}
onMounted(() => {
initImg()
})
</script>
- このとき、同じ画像が 2 つ表示されるので、キャンバスの fill メソッドと Clip メソッドを使用して画像を塗りつぶしたり切り抜いたりする必要もあります。
2 つのスライス領域を作成する方法
js复制代码export interface IProps {
width?: number
height?: number
l?: number
r?: number
}
const props = withDefaults(defineProps<IProps>(), {
width: 320,
height: 160,
l: 42,
r: 9
})
const drawPath = (ctx: any, x: number, y: number, operation: "fill" | "clip") => {
ctx.beginPath()
ctx.moveTo(x, y)
ctx.arc(x + props.l / 2, y - props.r + 2, props.r, 0.72 * PI, 2.26 * PI)
ctx.lineTo(x + props.l, y)
ctx.arc(x + props.l + props.r - 2, y + props.l / 2, props.r, 1.21 * PI, 2.78 * PI)
ctx.lineTo(x + props.l, y + props.l)
ctx.lineTo(x, y + props.l)
ctx.arc(x + props.r - 2, y + props.l / 2, props.r + 0.4, 2.76 * PI, 1.24 * PI, true)
ctx.lineTo(x, y)
ctx.lineWidth = 2
ctx.fillStyle = "rgba(255, 255, 255, 0.7)"
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)"
ctx.stroke()
ctx.globalCompositeOperation = "destination-over"
operation === "fill" ? ctx.fill() : ctx.clip()
}
const draw = (img: HTMLImageElement) => {
if (canvasRef.value && blockRef.value) {
const canvasCtx = canvasRef.value.getContext("2d") as CanvasRenderingContext2D
const blockCtx = blockRef.value.getContext("2d") as CanvasRenderingContext2D
// 随机位置创建拼图位置
xRef.value = getRandomNumberByRange(L + 10, props.width - (L + 10))
yRef.value = getRandomNumberByRange(10 + props.r * 2, props.height - (L + 10))
drawPath(canvasCtx, xRef.value, yRef.value, "fill")
drawPath(blockCtx, xRef.value, yRef.value, "clip")
canvasCtx.drawImage(img, 0, 0, props.width, props.height)
blockCtx.drawImage(img, 0, 0, props.width, props.height)
}
}
ctx.globalCompositeOperation = "destination-over" このコードは、Canvas コンテキストでグローバル合成操作モード (globalCompositeOperation) を設定します。グローバル合成操作モードとして destination-over を指定します。これは、描画された新しい形状を既存の形状にオーバーレイする方法を定義します。
具体的には、宛先オーバー操作モードでは、新しく描画された形状が既存の形状の下に配置されます。つまり、新しい形状は既存の形状の背後に描画されます。つまり、この操作モードを使用すると、最初に既存のシェイプを描画し、次に既存のシェイプの下に新しいシェイプを描画して、マスクまたはカスケード効果を作成できます。
このコードでは、ctx.globalCompositeOperation = "destination-over" の目的は、後続の描画操作 (ctx.fill() または ctx.clip()) が現在描画されているグラフィックスの後ろから表示されるようにすることである可能性があります。この時の効果は以下の通りです。
一番下の一番下のスライスを移動する
js复制代码const draw = (img: HTMLImageElement) => {
if (canvasRef.value && blockRef.value) {
...
// 提取滑块并放到最左边
const y1 = yRef.value - props.r * 2 - 1
// x,y,width,height
const ImageData = blockCtx.getImageData(xRef.value - 3, y1, L, L)
blockRef.value.width = L
blockCtx.putImageData(ImageData, 0, y1)
}
}
- getRandomNumberByRange() 関数を呼び出すと、指定された範囲内でランダムな x 座標と y 座標が生成され、パズルの位置を決定するために使用されます。
- drawPath() 関数を呼び出して、元の画像 Canvas とスライダー Canvas にパスを描画します。「fill」タイプと「clip」タイプを使用してそれぞれパスを塗りつぶし、パズルの視覚効果を作成します。
- Image() メソッドを使用して、元の画像 Canvas およびスライダー Canvas に画像を描画します。読み込んだ画像を Canvas コンテキストに描画します。
- getImageData() メソッドを使用して、スライダー キャンバスからスライダーの画像データを抽出します。このうち、抽出領域の始点のx座標はxRef.value - 3、y座標はy1、幅と高さはともにLです。
- `putImageData メソッドを使用して、抽出した画像データをスライダー Canvas に戻し、左側の位置、つまりスライダーを左端に配置します。
このうち、const y1 = yRef.value - props.r * 2 - 1 を使用して、スライダー上部の y 座標 (yRef.value) を計算し、スライダーの高さ (props.r * 2) を減算します。変位(-1)を減算すると、スライダー抽出領域の始点のy座標y1が得られます。この値は、スライダー Canvas から画像データを抽出する四角形領域の位置を決定するために使用されます。
このとき、絶対配置により、canvasRefが含まれないように下の画像をスライスします。
css复制代码.block {
position: absolute;
top: 0;
left: 0;
cursor: pointer;
cursor: grab;
}
ドラッグアンドドロップの実装方法
复制代码 class="vertifyWrap"
:style="{
width: props.width + 'px',
margin: '0 auto'
}"
@mouseup="handleDragEnd"
@mousemove="handleDragMove"
>
<div class="canvasArea">
<canvas :width="width" :height="height" ref="canvasRef" />
<canvas ref="blockRef" :width="width" :height="height" class="block" @mousedown="handleDragStart" />
</div>
</div>
const handleDragStart = (e: MouseEvent) => {
originXRef.value = e.clientX
originYRef.value = e.clientY
isMouseDownRef.value = true
}
const handleDragMove = (e: MouseEvent) => {
if (!isMouseDownRef.value) return false
e.preventDefault()
const eventX = e.clientX
const moveX = eventX - originXRef.value
if (moveX < 0 || moveX + 40 + 20 >= props.width) return false
const blockLeft = moveX
if (blockRef.value) {
blockRef.value.style.left = blockLeft + "px"
}
}
const handleDragEnd = (e: MouseEvent) => {
if (!isMouseDownRef.value) return false
isMouseDownRef.value = false
const eventX = e.clientX
if (eventX === originXRef.value) return false
}
Mousemove を vertifyWrap にバインドする目的は、ドラッグ プロセス中にポインターがすぐに失われてドラッグできなくなるのを防ぐことです。handleDragMove() 関数でドラッグ境界値を計算するだけです。
下部スライダーのドラッグ
js复制代码 <div
:class="sliderClass"
:style="{
width: width + 'px'
}"
>
<div class="sliderMask" :style="{ width: sliderLeft + 'px' }">
<div class="slider" :style="{ left: sliderLeft + 'px' }" @mousedown="handleDragStart">
<div className="sliderIcon">→</div>
</div>
</div>
</div>
const handleDragMove = (e: MouseEvent) => {
if (!isMouseDownRef.value) return false
e.preventDefault()
const eventX = e.clientX
const moveX = eventX - originXRef.value
if (moveX < 0 || moveX + 40 + 20 >= props.width) return false
sliderLeft.value = moveX
const blockLeft = moveX
if (blockRef.value) {
blockRef.value.style.left = blockLeft + "px"
}
}
sliderMask の sliderLeft 変数をバインドして、下部スライダーの左オフセット値を制御しますが、ドラッグ処理中に次の問題が発生します。
現在表示されている領域が props.width であるため、下の四角形が上の四角形と揃っていないことがわかりました。したがって、sliderLeft と blockLeft を同じ値にすることはできません。
js复制代码const handleDragMove = (e: MouseEvent) => {
if (!isMouseDownRef.value) return false
e.preventDefault()
const eventX = e.clientX
const moveX = eventX - originXRef.value
if (moveX < 0 || moveX + 40 >= props.width) return false
sliderLeft.value = moveX
const blockLeft = ((props.width - 40 - 20) / (props.width - 40)) * moveX
if (blockRef.value) {
blockRef.value.style.left = blockLeft + "px"
}
}
リフレッシュ機能の追加
js复制代码const handleRefresh = () => {
reset()
}
const reset = () => {
if (canvasRef.value && blockRef.value) {
const canvasCtx = canvasRef.value.getContext("2d") as CanvasRenderingContext2D
const blockCtx = blockRef.value.getContext("2d") as CanvasRenderingContext2D
// 重置样式
sliderLeft.value = 0
sliderClass.value = "sliderContainer"
blockRef.value.width = props.width
blockRef.value.style.left = 0 + "px"
// 清空画布
canvasCtx.clearRect(0, 0, props.width, props.height)
blockCtx.clearRect(0, 0, props.width, props.height)
// 重新加载图片
console.log(getRandomImgSrc(), "getRandomImgSrc()")
imgRef.value.setSrc(getRandomImgSrc())
}
}
ここまででスライドコンポーネントの核となるコードは完成しましたが、この過程でctx.fill()とctx.clip()の使い方を学び、比較的簡単だったcanvasに絵を描くスキルも学びました。やりがいのある。