背景
先上地址:https://kifuan.github.io/photo-cutter/
由于某群友有切割图片放到个人资料里面的需求,所以我就顺手写了一个这样的项目。
效果
如下:
原理
来源: MDN
利用CanvasRenderingContext2D.drawImage
对图片进行处理,API如下:
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
用下面这张图就可以解释清楚:
因为我们并不需要调整这么多的参数,举个栗子,把一张300x400
的从100, 200
的位置截出一个100x100
的图片,我们需要这么做:
ctx.drawImage(image, 100, 200, 100, 100, 0, 0, 100, 100)
并不需要记住每个参数的位置,毕竟记不住可以查文档嘛。
实现
目标
目标其实很简单,我们要把一张大图切割成下面的形式:
忽略我作画的渣水平,就当每个格子都是正方形,所以说它的宽高比应当是3:4
。
策略模式的应用
因为我们有多种切割图片的策略,所以这里可以应用策略模式。
说白了就是把那一堆参数放到一个对象里面,如下:
代码语言:javascript复制// src/stores/strategy.ts
export interface Strategy {
// 显示在下拉框里的文本
label: string
// 处理时以 unit * 理想宽度 为一个单位
unit: number
// 理想宽高比
scale: number
// 每个步骤
steps: StrategyStep[]
}
export interface StrategyStep {
// 显示在canvas上方的文本
label: string
// 图片的长和宽是几个实际单位长度
size: number
// 图片x y坐标的offset
offset: [number, number]
}
export const strategies: Record<string, Strategy> = {
qq3x3: {
label: 'QQ个人资料图片3x3',
// 由于横着切三份,所以单位长度是三分之一
unit: 0.333333,
// 理想宽高比为3:4
scale: 0.75,
steps: [
{
label: '左1',
size: 2,
offset: [0, 0],
},
{
label: '右1',
size: 1,
offset: [2, 0],
},
{
label: '右2',
size: 1,
offset: [2, 1],
},
{
label: '左2',
size: 1,
offset: [0, 2],
},
{
label: '中1',
size: 1,
offset: [1, 2],
},
{
label: '右3',
size: 1,
offset: [2, 2],
},
{
label: '左3',
size: 1,
offset: [0, 3],
},
{
label: '中2',
size: 1,
offset: [1, 3],
},
{
label: '右4',
size: 1,
offset: [2, 3],
},
],
},
// 另外两个略,可以自行去Github仓库查看
}
计算标准长度
看起来挺吓人,实际上核心很简单,就是根据期望比例算出来标准数据,实际上是一个数学问题。
代码语言:javascript复制// src/components/PhotoFragments.vue
// 源码中没有这个类型,为了在本文中显得美观一点,我就又提出来了一个Data类型
interface Data {
unit: number
cutOffset: [number, number]
}
function calcRegularData(image: HTMLImageElement, strategy: Strategy) : Data {
const scale = image.width / image.height
if (scale > strategy.scale) {
// 实际宽高比 > 理想宽高比,意味着这个图片太宽了
// 所以用实际长度 * 理想宽高比 算出来理想宽度
const idealWidth = image.height * strategy.scale
// 相应地,单位长度就是根据理想宽度算出来的了
const unit = idealWidth * strategy.unit
// 图片太宽了,我们保留中间部分,裁掉左右两边
const cutOffsetX = (image.width - idealWidth) / 2
return { unit, cutOffset: [ cutOffsetX, 0 ] }
}
else {
// 走到这里意味着这个图片太窄了
// 所以用实际宽度 / 理想宽高比 算出来理想高度
const idealHeight = image.width / strategy.scale
// 因为宽度是符合要求的,所以直接用实际宽度计算出单位长度
const unit = image.width * strategy.unit
// 图片太窄了,裁掉上下两边
const cutOffsetY = (image.height - idealHeight) / 2
return { unit, cutOffset: [ 0, cutOffsetY ] }
}
}
画到canvas上
在此之前,我们先给出Vue模板代码,用来动态生成canvas
:
<template>
<div v-for="(step, index) in strategy.steps" :key="index" class="fragment">
<p>{{ step.label }}</p>
<canvas
:ref="el => { canvases[index] = el as HTMLCanvasElement }"
/>
<button @click="handleDownload(index)">
下载
</button>
</div>
</template>
<style scoped>
.fragment {
display: flex;
flex-direction: column;
}
.fragment > * {
margin-bottom: 15px;
}
</style>
在这里,我向ref
里面传了一个函数,它可以把canvas
的DOM
保存到canvases
这个数组里面。
因为前面还定义了一些变量,这里不方便进行代码块的截取,索性都贴出来了,除了calcRegularData
这个函数。
// src/components/PhotoFragments.vue
import { storeToRefs } from 'pinia'
import { computed, watch } from 'vue'
import { useImageStore } from '../stores/image'
import { strategies, Strategy, useStrategyStore } from '../stores/strategy'
// 下面这些都是pinia的store
const imageStore = useImageStore()
const strategyStore = useStrategyStore()
const strategy = computed(() => {
return strategies[strategyStore.strategy]
})
const canvases = [] as HTMLCanvasElement[]
watch(storeToRefs(imageStore).image, (image) => {
if (image === undefined)
return
// 先把标准长度计算出来
const { unit, cutOffset: [ cutOffsetX, cutOffsetY ] } = calcRegularData(image, strategy.value)
for (let i = 0; i < canvases.length; i ) {
const { size, offset: [offsetX, offsetY] } = strategy.value.steps[i]
const canvas = canvases[i]
const ctx = canvas.getContext('2d')!
// 先设置canvas的长宽
canvas.width = canvas.height = size * unit
// 把这个图片根据参数画到canvas上
ctx.drawImage(image, unit * offsetX cutOffsetX, unit * offsetY cutOffsetY,
unit * size, unit * size, 0, 0, unit * size, unit * size)
}
})
function handleDownload(canvasIndex: number) {
const a = document.createElement('a')
// 下面这串代码是随机生成一个字符串,作为下载的文件名
a.download = Math.random().toString(36).substring(2)
a.href = canvases[canvasIndex].toDataURL()
a.click()
}
就这样,核心代码就这些,store
定义的部分我就省去了,都是大同小异的代码。