<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<!-- 引入 vue 入口文件 -->
<script type="module" src="./main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
index.html
main.js
App.vue
package.json
md
项目介绍.md
BezierCurveEditor.vue
现在支持上传本地图片了!
index.html
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
console.log(["Hello 笔.COOL 控制台"])
App.vue
<template>
<div>
<BezierCurveEditor v-model="bezierPoints" :width="255" :height="255" />
<div>{{bezierPoints}}</div>
</div>
</template>
<script setup>
import { ref } from "vue"
import BezierCurveEditor from './BezierCurveEditor.vue'
const bezierPoints = ref([
{ x: 0, y: 0 },
{ x: 20, y: 180 },
{ x: 200, y: 60 },
{ x: 255, y: 255 }
])
</script>
package.json
注意:新添加的依赖包首次加载可能会报错,稍后再次刷新即可
{
"dependencies": {
"vue": "3.5.15"
}
}
项目介绍.md
# 笔.COOL编辑器 - Vue3 项目模板
## 介绍与用途
- 基础 Vue 项目,方便用户快速上手使用 **笔.COOL编辑器**
- 适合想要简单练习 Vue
## 文件
`index.html`: HTML 入口文件,内容包含 Vue 实例所挂载的 root 节点 `<div id="app"></div>`
`main.js`: vue 主入口 js 文件,负责初始化 Vue 实例,通过 `index.html` 引入
`global.css`: 全局样式文件,通过 `main.js` 引入
`App.vue`: App组件
`HelloBicool`: HelloBicool组件
`package.json`: npm 包管理文件,你可以继续添加其他依赖包 **【注意:首次引入新依赖可能会报错,稍后再次刷新即可】**
## 内容
- 标题 `"Hello 笔.COOL"`
- 副标题 `"一笔一划,绘就人生;一码一境,酷创未来。"`
- 显示一个 Vue 图标
- 控制台输出 `["Hello 笔.COOL 控制台"]`
## 模板使用
- 首页点击 **`创建项目`**,选择 **`新建 Vue3 项目`** 即可使用该模板
- 或访问 https://bi.cool/project?template=vue_3_5_js 即可快速进入此模版
BezierCurveEditor.vue
<template>
<div id="app">
<svg ref="svgElement" :width="svgWidth" :height="svgHeight"
:viewBox="`-${padding} -${padding} ${svgWidth + padding * 2} ${svgHeight + padding * 2}`">
<!-- 贝塞尔曲线 -->
<path v-if="controlLines.length === 2"
:d="`M ${points[0]!.x} ${svgHeight - points[0]!.y} C ${points[1]!.x} ${svgHeight - points[1]!.y}, ${points[2]!.x} ${svgHeight - points[2]!.y}, ${points[3]!.x} ${svgHeight - points[3]!.y}`"
fill="none" stroke="black" stroke-width="2" />
<!-- 控制线(虚线) -->
<line v-for="(line, index) in controlLines" :key="'line-' + index" :x1="line.x1" :y1="svgHeight - line.y1"
:x2="line.x2" :y2="svgHeight - line.y2" stroke="gray" stroke-dasharray="5,5" />
<!-- 绘制固定点(红色) -->
<circle v-for="index in [0, 3]" :key="'fixed-' + index" :cx="points[index]!.x" :cy="svgHeight - points[index]!.y"
r="7" fill="red" />
<!-- 绘制控制点(蓝色) -->
<circle v-for="index in [1, 2]" :key="'control-' + index" :cx="points[index]!.x"
:cy="svgHeight - points[index]!.y" r="7" fill="blue" @mousedown="onMouseDown($event, index)"
class="control-point" />
</svg>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue'
interface Point {
x: number;
y: number;
}
interface Line {
x1: number;
y1: number;
x2: number;
y2: number;
}
const padding = 40
const props = defineProps({
modelValue: {
type: Array as () => Point[],
default: () => [
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 500, y: 500 }
]
},
width: {
type: Number,
default: 500
},
height: {
type: Number,
default: 500
}
})
const emit = defineEmits<{
'update:modelValue': [value: Point[]]
}>()
const svgElement = ref<SVGSVGElement | null>(null)
const svgWidth = ref<number>(props.width)
const svgHeight = ref<number>(props.height)
const draggingIndex = ref<number | null>(null)
const points = ref<Point[]>(props.modelValue.map(p => ({ x: p.x, y: p.y })))
// 只监听内部 points 变化并 emit
watch(points, (newVal) => {
emit('update:modelValue', newVal.map(p => ({ x: p.x, y: p.y })))
}, { deep: true })
const controlLines = computed<Line[]>(() => {
// 运行时检查 points 数组的长度
if (points.value.length < 4) {
console.warn("Points array does not have enough elements to compute control lines.");
// 返回一个空数组或默认值
return [];
}
// TypeScript 现在知道 points.value 至少有 4 个元素了
return [
{
x1: points.value[0]?.x || 0,
y1: points.value[0]?.y || 0,
x2: points.value[1]?.x || 0,
y2: points.value[1]?.y || 0
},
{
x1: points.value[3]?.x || 0,
y1: points.value[3]?.y || 0,
x2: points.value[2]?.x || 0,
y2: points.value[2]?.y || 0
}
]
})
const getSVGPoint = (clientX: number, clientY: number): DOMPoint => {
if (!svgElement.value) return new DOMPoint(0, 0)
const svg = svgElement.value
const point = svg.createSVGPoint()
point.x = clientX
point.y = clientY
const ctm = svg.getScreenCTM()
if (!ctm) return new DOMPoint(0, 0)
const svgPoint = point.matrixTransform(ctm.inverse())
svgPoint.y = svgHeight.value - svgPoint.y
return svgPoint
}
const onMouseDown = (event: MouseEvent, index: number) => {
if (index === 1 || index === 2) {
event.preventDefault()
draggingIndex.value = index
document.addEventListener('mousemove', onMouseMoveGlobal)
document.addEventListener('mouseup', onMouseUpGlobal)
document.body.style.userSelect = 'none'
}
}
const onMouseMoveGlobal = (event: MouseEvent) => {
if (draggingIndex.value !== null) {
const svgPoint = getSVGPoint(event.clientX, event.clientY)
const roundedX = Math.round(svgPoint.x)
const roundedY = Math.round(svgPoint.y)
const boundedX = Math.max(0, Math.min(roundedX, svgWidth.value))
const boundedY = Math.max(0, Math.min(roundedY, svgHeight.value))
// 直接修改响应式对象的属性
points.value[draggingIndex.value]!.x = boundedX
points.value[draggingIndex.value]!.y = boundedY
}
}
const onMouseUpGlobal = () => {
draggingIndex.value = null
document.removeEventListener('mousemove', onMouseMoveGlobal)
document.removeEventListener('mouseup', onMouseUpGlobal)
document.body.style.userSelect = ''
}
onUnmounted(() => {
document.removeEventListener('mousemove', onMouseMoveGlobal)
document.removeEventListener('mouseup', onMouseUpGlobal)
document.body.style.userSelect = ''
})
</script>
<style scoped>
#app {
padding: 50px;
}
svg {
border: 1px solid #ccc;
cursor: pointer;
}
circle {
transition: r 0.2s, fill 0.2s;
}
.control-point {
cursor: move;
}
.control-point:hover {
r: 9;
fill: #0066ff;
}
line {
stroke-width: 1.5;
}
path {
transition: stroke 0.3s;
}
</style>
预览页面