0.3.0: freeform drawing, stroke properties, polygon edit gestures
- Freeform drawing tool: new sidebar button enters a click-to-draw mode that appends points, shows a live ghost preview line, and exits via Enter or double-click (near the start point closes the path). Works end-to-end through all four render pipelines. - Polygon authoring gestures: double-click on a path inserts a vertex on the nearest edge; double-click on an open polygon's endpoint resumes the drawing session from that end, reversing point order when needed. - Stroke + closed-path properties: all shape types (rect, circle, text, polygon) expose stroke color, stroke width, and (for polygons) a closed toggle. Fill and stroke each support a "transparent" switch so shapes can render as outline-only or fill-only. - Mask preview no longer inherits the user's stroke — the dashed ghost keeps its own outline regardless of stroke-width changes. - Playhead no longer dims on inactive timeline tracks; only keyframes, segments and clip blocks fade. - Properties panel point-count control for polygons, plus per-keyframe points animation that correctly interpolates once a baseline exists. - Pivot drag hides the rotation handle until release. - Transformer attach retries on a short timer to avoid races where the Konva node hasn't mounted yet when selection changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a05396e734
commit
276d621d20
13 files changed
+944
-64
No files matched your search
+333
-19
@@ -318,6 +318,17 @@ function makeCircleSceneFunc(blur: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function strokeCfg(
|
||||
props: AnimatableProps,
|
||||
asset: { stroke?: string; strokeWidth?: number },
|
||||
): Record<string, unknown> {
|
||||
const strokeWidth = props.strokeWidth ?? asset.strokeWidth ?? 0
|
||||
if (strokeWidth <= 0) return { strokeWidth: 0, stroke: undefined }
|
||||
const stroke = (props.stroke ?? asset.stroke) || '#000000'
|
||||
if (stroke === 'transparent') return { strokeWidth: 0, stroke: undefined }
|
||||
return { stroke, strokeWidth, strokeScaleEnabled: false }
|
||||
}
|
||||
|
||||
function rectConfig(layer: Layer, asset: Extract<Asset, { kind: 'rect' }>, props: AnimatableProps) {
|
||||
const fill = props.fill ?? asset.fill
|
||||
const blur = props.blur ?? asset.blur ?? 0
|
||||
@@ -327,8 +338,9 @@ function rectConfig(layer: Layer, asset: Extract<Asset, { kind: 'rect' }>, props
|
||||
height: asset.height,
|
||||
offsetX: layer.offsetX ?? asset.width / 2,
|
||||
offsetY: layer.offsetY ?? asset.height / 2,
|
||||
fill,
|
||||
...(layer.isMask ? {} : strokeCfg(props, asset)),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (blur > 0) cfg.sceneFunc = makeRectSceneFunc(blur)
|
||||
return cfg
|
||||
}
|
||||
@@ -345,21 +357,25 @@ function circleConfig(
|
||||
radius: asset.radius,
|
||||
offsetX: layer.offsetX ?? 0,
|
||||
offsetY: layer.offsetY ?? 0,
|
||||
fill,
|
||||
...(layer.isMask ? {} : strokeCfg(props, asset)),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (blur > 0) cfg.sceneFunc = makeCircleSceneFunc(blur)
|
||||
return cfg
|
||||
}
|
||||
|
||||
function textConfig(layer: Layer, asset: Extract<Asset, { kind: 'text' }>, props: AnimatableProps) {
|
||||
return {
|
||||
const fill = props.fill ?? asset.fill
|
||||
const cfg: Record<string, unknown> = {
|
||||
...baseNodeConfig(layer, props),
|
||||
text: asset.text,
|
||||
fontSize: asset.fontSize,
|
||||
offsetX: layer.offsetX ?? 0,
|
||||
offsetY: layer.offsetY ?? 0,
|
||||
fill: props.fill ?? asset.fill,
|
||||
...(layer.isMask ? {} : strokeCfg(props, asset)),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
return cfg
|
||||
}
|
||||
|
||||
function polygonConfig(
|
||||
@@ -368,10 +384,14 @@ function polygonConfig(
|
||||
props: AnimatableProps,
|
||||
) {
|
||||
const fill = props.fill ?? asset.fill
|
||||
const rawStroke = props.stroke ?? asset.stroke
|
||||
const strokeWidth = props.strokeWidth ?? asset.strokeWidth ?? 0
|
||||
const stroke = strokeWidth > 0 ? (rawStroke || '#000000') : undefined
|
||||
const blur = props.blur ?? asset.blur ?? 0
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const pts = props.points ?? asset.points
|
||||
const closed = asset.closed !== false
|
||||
const flat: number[] = []
|
||||
for (const p of pts) {
|
||||
flat.push(p.x - offX, p.y - offY)
|
||||
@@ -379,9 +399,21 @@ function polygonConfig(
|
||||
const cfg: Record<string, unknown> = {
|
||||
...baseNodeConfig(layer, props),
|
||||
points: flat,
|
||||
closed: true,
|
||||
fill,
|
||||
closed,
|
||||
lineJoin: 'round',
|
||||
lineCap: 'round',
|
||||
}
|
||||
if (closed && fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (!layer.isMask && stroke && strokeWidth > 0 && stroke !== 'transparent') {
|
||||
cfg.stroke = stroke
|
||||
cfg.strokeWidth = strokeWidth
|
||||
cfg.strokeScaleEnabled = false
|
||||
} else {
|
||||
cfg.stroke = undefined
|
||||
cfg.strokeWidth = 0
|
||||
}
|
||||
// Widen hit area on edges so double-click to insert a vertex is easy
|
||||
cfg.hitStrokeWidth = Math.max(strokeWidth, 14 / zoom.value)
|
||||
if (blur > 0) {
|
||||
cfg.sceneFunc = (ctx: KonvaCtx, shape: KonvaShape) => {
|
||||
ctx.save()
|
||||
@@ -391,7 +423,7 @@ function polygonConfig(
|
||||
ctx.moveTo(pts[0].x - offX, pts[0].y - offY)
|
||||
for (let j = 1; j < pts.length; j++) ctx.lineTo(pts[j].x - offX, pts[j].y - offY)
|
||||
}
|
||||
ctx.closePath()
|
||||
if (closed) ctx.closePath()
|
||||
ctx.fillStrokeShape(shape)
|
||||
ctx.restore()
|
||||
}
|
||||
@@ -442,8 +474,9 @@ function clipRectConfig(item: RenderedClipLayer) {
|
||||
height: asset.height,
|
||||
offsetX: item.layer.offsetX ?? asset.width / 2,
|
||||
offsetY: item.layer.offsetY ?? asset.height / 2,
|
||||
fill,
|
||||
...strokeCfg(item.props, asset),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (blur > 0) cfg.sceneFunc = makeRectSceneFunc(blur)
|
||||
return cfg
|
||||
}
|
||||
@@ -457,22 +490,26 @@ function clipCircleConfig(item: RenderedClipLayer) {
|
||||
radius: asset.radius,
|
||||
offsetX: item.layer.offsetX ?? 0,
|
||||
offsetY: item.layer.offsetY ?? 0,
|
||||
fill,
|
||||
...strokeCfg(item.props, asset),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (blur > 0) cfg.sceneFunc = makeCircleSceneFunc(blur)
|
||||
return cfg
|
||||
}
|
||||
|
||||
function clipTextConfig(item: RenderedClipLayer) {
|
||||
const asset = item.asset as Extract<Asset, { kind: 'text' }>
|
||||
return {
|
||||
const fill = item.props.fill ?? asset.fill
|
||||
const cfg: Record<string, unknown> = {
|
||||
...clipBaseConfig(item.instanceId, item.layer, item.props),
|
||||
text: asset.text,
|
||||
fontSize: asset.fontSize,
|
||||
offsetX: item.layer.offsetX ?? 0,
|
||||
offsetY: item.layer.offsetY ?? 0,
|
||||
fill: item.props.fill ?? asset.fill,
|
||||
...strokeCfg(item.props, asset),
|
||||
}
|
||||
if (fill && fill !== 'transparent') cfg.fill = fill
|
||||
return cfg
|
||||
}
|
||||
|
||||
function clipPolygonConfig(item: RenderedClipLayer) {
|
||||
@@ -480,16 +517,28 @@ function clipPolygonConfig(item: RenderedClipLayer) {
|
||||
const offX = item.layer.offsetX ?? 0
|
||||
const offY = item.layer.offsetY ?? 0
|
||||
const pts = item.props.points ?? asset.points
|
||||
const closed = asset.closed !== false
|
||||
const stroke = item.props.stroke ?? asset.stroke
|
||||
const strokeWidth = item.props.strokeWidth ?? asset.strokeWidth ?? 0
|
||||
const fill = item.props.fill ?? asset.fill
|
||||
const flat: number[] = []
|
||||
for (const p of pts) {
|
||||
flat.push(p.x - offX, p.y - offY)
|
||||
}
|
||||
return {
|
||||
const cfg: Record<string, unknown> = {
|
||||
...clipBaseConfig(item.instanceId, item.layer, item.props),
|
||||
points: flat,
|
||||
closed: true,
|
||||
fill: item.props.fill ?? asset.fill,
|
||||
closed,
|
||||
lineJoin: 'round',
|
||||
lineCap: 'round',
|
||||
}
|
||||
if (closed && fill && fill !== 'transparent') cfg.fill = fill
|
||||
if (stroke && strokeWidth > 0) {
|
||||
cfg.stroke = stroke
|
||||
cfg.strokeWidth = strokeWidth
|
||||
cfg.strokeScaleEnabled = false
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
function clipImageConfig(item: RenderedClipLayer) {
|
||||
@@ -654,7 +703,15 @@ function onVertexDragEnd(e: Konva.KonvaEventObject<DragEvent>, index: number) {
|
||||
onVertexDragMove(e, index)
|
||||
}
|
||||
|
||||
|
||||
const draggingPivot = ref(false)
|
||||
|
||||
function onPivotDragStart() {
|
||||
draggingPivot.value = true
|
||||
}
|
||||
|
||||
function onPivotDragEnd(e: Konva.KonvaEventObject<DragEvent>) {
|
||||
draggingPivot.value = false
|
||||
const info = pivotInfo.value
|
||||
if (!info) return
|
||||
const node = e.target
|
||||
@@ -693,7 +750,13 @@ const transformerConfig = {
|
||||
ignoreStroke: true,
|
||||
}
|
||||
|
||||
function attachTransformer() {
|
||||
let attachRetryTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function attachTransformer(retriesLeft = 3) {
|
||||
if (attachRetryTimer) {
|
||||
clearTimeout(attachRetryTimer)
|
||||
attachRetryTimer = null
|
||||
}
|
||||
const stage = stageRef.value?.getStage()
|
||||
const tr = transformerRef.value?.getNode()
|
||||
if (!stage || !tr) return
|
||||
@@ -703,18 +766,34 @@ function attachTransformer() {
|
||||
tr.getLayer()?.batchDraw()
|
||||
return
|
||||
}
|
||||
// Polygons are edited via vertex handles — no bulk transformer
|
||||
const layer = store.activeLayers.find((l) => l.id === id)
|
||||
if (layer) {
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (asset?.kind === 'polygon') {
|
||||
tr.nodes([])
|
||||
tr.getLayer()?.batchDraw()
|
||||
return
|
||||
}
|
||||
}
|
||||
const node = stage.findOne<Konva.Node>(`#${id}`)
|
||||
if (!node && retriesLeft > 0) {
|
||||
// The Konva node for a freshly-rendered layer may not be attached yet.
|
||||
// Retry on the next animation frame a few times before giving up.
|
||||
attachRetryTimer = setTimeout(() => attachTransformer(retriesLeft - 1), 16)
|
||||
return
|
||||
}
|
||||
tr.nodes(node ? [node] : [])
|
||||
tr.getLayer()?.batchDraw()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [store.selectedLayerId, stackItems.value.length, store.playhead] as const,
|
||||
() => [store.selectedLayerId, stackItems.value, store.playhead] as const,
|
||||
async () => {
|
||||
await nextTick()
|
||||
attachTransformer()
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
function updateLivePose(node: Konva.Node, layerId: string) {
|
||||
@@ -742,7 +821,46 @@ function handleTransform(e: Konva.KonvaEventObject<Event>) {
|
||||
updateLivePose(node, id)
|
||||
}
|
||||
|
||||
const drawingPreview = ref<{ x: number; y: number } | null>(null)
|
||||
|
||||
function pointerToWorld(e: Konva.KonvaEventObject<MouseEvent>) {
|
||||
const stage = stageRef.value?.getStage()
|
||||
if (!stage) return null
|
||||
const pos = stage.getPointerPosition()
|
||||
if (!pos) return null
|
||||
return {
|
||||
x: (pos.x - viewX.value) / zoom.value,
|
||||
y: (pos.y - viewY.value) / zoom.value,
|
||||
}
|
||||
}
|
||||
|
||||
function handleStageMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
|
||||
if (store.drawingMode === 'freeform') {
|
||||
const native = e.evt as MouseEvent
|
||||
// Middle-click or Ctrl+click belong to the pan gesture, not drawing
|
||||
if (native.button === 1 || native.ctrlKey) return
|
||||
const wp = pointerToWorld(e)
|
||||
if (!wp) return
|
||||
const layerId = store.drawingLayerId
|
||||
const layer = layerId ? store.activeLayers.find((l) => l.id === layerId) : null
|
||||
const local = layer ? worldToPolygonLocal(layer, wp.x, wp.y) : wp
|
||||
// Close-on-click near first point (compare in asset-local space)
|
||||
const assetId = store.drawingAssetId
|
||||
const asset = assetId ? store.scene.assets.find((a) => a.id === assetId) : null
|
||||
if (asset && asset.kind === 'polygon' && asset.points.length >= 3) {
|
||||
const sampled = layer ? sampleLayer(layer, store.playhead) : null
|
||||
const scale = sampled ? Math.max(Math.abs(sampled.scaleX), Math.abs(sampled.scaleY), 0.01) : 1
|
||||
const first = asset.points[0]
|
||||
const threshold = 10 / zoom.value / scale
|
||||
if (Math.hypot(local.x - first.x, local.y - first.y) <= threshold) {
|
||||
store.setPolygonClosed(asset.id, true)
|
||||
store.finishDrawing()
|
||||
return
|
||||
}
|
||||
}
|
||||
store.addDrawingPoint(local.x, local.y)
|
||||
return
|
||||
}
|
||||
const target = e.target
|
||||
const tr = transformerRef.value?.getNode()
|
||||
if (tr) {
|
||||
@@ -768,6 +886,180 @@ function handleStageMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
|
||||
store.selectLayer(match ? match.id : null)
|
||||
}
|
||||
|
||||
function handleStageMouseMove(e: Konva.KonvaEventObject<MouseEvent>) {
|
||||
if (store.drawingMode !== 'freeform') {
|
||||
drawingPreview.value = null
|
||||
return
|
||||
}
|
||||
const wp = pointerToWorld(e)
|
||||
if (!wp) return
|
||||
const layerId = store.drawingLayerId
|
||||
const layer = layerId ? store.activeLayers.find((l) => l.id === layerId) : null
|
||||
drawingPreview.value = layer ? worldToPolygonLocal(layer, wp.x, wp.y) : wp
|
||||
}
|
||||
|
||||
function onContainerDblClick(e: MouseEvent) {
|
||||
const rect = container.value?.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
const wx = (e.clientX - rect.left - viewX.value) / zoom.value
|
||||
const wy = (e.clientY - rect.top - viewY.value) / zoom.value
|
||||
|
||||
// While drawing: finish (optionally close if near first point)
|
||||
if (store.drawingMode === 'freeform') {
|
||||
const assetId = store.drawingAssetId
|
||||
const asset = assetId ? store.scene.assets.find((a) => a.id === assetId) : null
|
||||
if (asset && asset.kind === 'polygon' && asset.points.length >= 3) {
|
||||
const first = asset.points[0]
|
||||
const threshold = 12 / zoom.value
|
||||
if (Math.hypot(wx - first.x, wy - first.y) <= threshold) {
|
||||
store.setPolygonClosed(asset.id, true)
|
||||
}
|
||||
}
|
||||
store.finishDrawing()
|
||||
return
|
||||
}
|
||||
|
||||
// Not drawing: polygon-specific gestures on selected polygon
|
||||
const selId = store.selectedLayerId
|
||||
if (!selId) return
|
||||
const layer = store.activeLayers.find((l) => l.id === selId)
|
||||
if (!layer || layer.locked) return
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset || asset.kind !== 'polygon') return
|
||||
const sampled = sampleLayer(layer, store.playhead)
|
||||
const pts = sampled.points ?? asset.points
|
||||
if (pts.length < 1) return
|
||||
const closed = asset.closed !== false
|
||||
const local = worldToPolygonLocal(layer, wx, wy)
|
||||
|
||||
// Near an endpoint of an open polygon → resume drawing
|
||||
if (!closed) {
|
||||
const first = pts[0]
|
||||
const last = pts[pts.length - 1]
|
||||
const threshold = 14 / zoom.value
|
||||
const nearFirst = Math.hypot(local.x - first.x, local.y - first.y) <= threshold
|
||||
const nearLast =
|
||||
pts.length > 1 && Math.hypot(local.x - last.x, local.y - last.y) <= threshold
|
||||
if (nearLast || nearFirst) {
|
||||
if (nearFirst && !nearLast) store.reversePolygonPoints(asset.id)
|
||||
store.startFreeformDrawing()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Click on (or near) an edge → insert a new vertex there
|
||||
const edges = closed ? pts.length : pts.length - 1
|
||||
if (edges < 1) return
|
||||
let bestIdx = 1
|
||||
let bestDist = Infinity
|
||||
let bestT = 0
|
||||
for (let i = 0; i < edges; i++) {
|
||||
const a = pts[i]
|
||||
const b = pts[(i + 1) % pts.length]
|
||||
const dx = b.x - a.x
|
||||
const dy = b.y - a.y
|
||||
const lenSq = dx * dx + dy * dy
|
||||
if (lenSq === 0) continue
|
||||
let t = ((local.x - a.x) * dx + (local.y - a.y) * dy) / lenSq
|
||||
t = Math.max(0, Math.min(1, t))
|
||||
const px = a.x + t * dx
|
||||
const py = a.y + t * dy
|
||||
const d = Math.hypot(local.x - px, local.y - py)
|
||||
if (d < bestDist) {
|
||||
bestDist = d
|
||||
bestIdx = i + 1
|
||||
bestT = t
|
||||
}
|
||||
}
|
||||
const edgeThreshold = 14 / zoom.value
|
||||
if (bestDist <= edgeThreshold) {
|
||||
store.insertPolygonPointOnEdge(asset.id, bestIdx, bestT)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrawingKey(e: KeyboardEvent) {
|
||||
if (store.drawingMode !== 'freeform') return
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
store.cancelDrawing()
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
store.finishDrawing()
|
||||
}
|
||||
}
|
||||
|
||||
function transformLocalToWorld(
|
||||
layer: Layer,
|
||||
sampled: AnimatableProps,
|
||||
local: { x: number; y: number },
|
||||
): { x: number; y: number } {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const lx = (local.x - offX) * sampled.scaleX
|
||||
const ly = (local.y - offY) * sampled.scaleY
|
||||
const theta = (sampled.rotation * Math.PI) / 180
|
||||
const c = Math.cos(theta)
|
||||
const s = Math.sin(theta)
|
||||
return {
|
||||
x: sampled.x + lx * c - ly * s,
|
||||
y: sampled.y + lx * s + ly * c,
|
||||
}
|
||||
}
|
||||
|
||||
const drawingPreviewConfig = computed(() => {
|
||||
if (store.drawingMode !== 'freeform') return null
|
||||
const layerId = store.drawingLayerId
|
||||
if (!layerId) return null
|
||||
const layer = store.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return null
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset || asset.kind !== 'polygon') return null
|
||||
const sampled = sampleLayer(layer, store.playhead)
|
||||
const pts = sampled.points ?? asset.points
|
||||
const preview = drawingPreview.value
|
||||
const flat: number[] = []
|
||||
for (const p of pts) {
|
||||
const w = transformLocalToWorld(layer, sampled, p)
|
||||
flat.push(w.x, w.y)
|
||||
}
|
||||
if (preview && pts.length > 0) {
|
||||
const w = transformLocalToWorld(layer, sampled, preview)
|
||||
flat.push(w.x, w.y)
|
||||
}
|
||||
if (flat.length < 4) return null
|
||||
return {
|
||||
points: flat,
|
||||
stroke: '#6ea8ff',
|
||||
strokeWidth: 1.5 / zoom.value,
|
||||
dash: [6 / zoom.value, 4 / zoom.value],
|
||||
strokeScaleEnabled: false,
|
||||
listening: false,
|
||||
}
|
||||
})
|
||||
|
||||
const drawingDotsConfig = computed(() => {
|
||||
if (store.drawingMode !== 'freeform') return []
|
||||
const layerId = store.drawingLayerId
|
||||
if (!layerId) return []
|
||||
const layer = store.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return []
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset || asset.kind !== 'polygon') return []
|
||||
const sampled = sampleLayer(layer, store.playhead)
|
||||
const pts = sampled.points ?? asset.points
|
||||
return pts.map((p, i) => {
|
||||
const w = transformLocalToWorld(layer, sampled, p)
|
||||
return {
|
||||
key: `dp-${i}`,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
radius: 3 / zoom.value,
|
||||
fill: '#6ea8ff',
|
||||
listening: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function handleDragEnd(e: Konva.KonvaEventObject<DragEvent>) {
|
||||
const node = e.target
|
||||
const id = node.id()
|
||||
@@ -809,6 +1101,7 @@ const rotationHandlePos = ref<{ x: number; y: number } | null>(null)
|
||||
const rotationHandleConfig = computed(() => {
|
||||
const info = pivotInfo.value
|
||||
if (!info) return null
|
||||
if (draggingPivot.value) return null
|
||||
if (rotationHandlePos.value) {
|
||||
return {
|
||||
x: rotationHandlePos.value.x,
|
||||
@@ -830,6 +1123,7 @@ const rotationHandleConfig = computed(() => {
|
||||
const rotationArmConfig = computed(() => {
|
||||
const info = pivotInfo.value
|
||||
if (!info) return null
|
||||
if (draggingPivot.value) return null
|
||||
let endX: number
|
||||
let endY: number
|
||||
if (rotationHandlePos.value) {
|
||||
@@ -1069,6 +1363,8 @@ onMounted(() => {
|
||||
container.value.addEventListener('pointercancel', onContainerPointerUp)
|
||||
container.value.addEventListener('dragover', onContainerDragOver)
|
||||
container.value.addEventListener('drop', onContainerDrop)
|
||||
container.value.addEventListener('dblclick', onContainerDblClick)
|
||||
window.addEventListener('keydown', handleDrawingKey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -1082,12 +1378,19 @@ onBeforeUnmount(() => {
|
||||
container.value?.removeEventListener('pointercancel', onContainerPointerUp)
|
||||
container.value?.removeEventListener('dragover', onContainerDragOver)
|
||||
container.value?.removeEventListener('drop', onContainerDrop)
|
||||
container.value?.removeEventListener('dblclick', onContainerDblClick)
|
||||
window.removeEventListener('keydown', handleDrawingKey)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" class="stage-container">
|
||||
<v-stage ref="stageRef" :config="stageConfig" @mousedown="handleStageMouseDown">
|
||||
<div ref="container" class="stage-container" :class="{ drawing: store.drawingMode === 'freeform' }">
|
||||
<v-stage
|
||||
ref="stageRef"
|
||||
:config="stageConfig"
|
||||
@mousedown="handleStageMouseDown"
|
||||
@mousemove="handleStageMouseMove"
|
||||
>
|
||||
<v-layer>
|
||||
<v-group :config="viewConfig">
|
||||
<v-group :config="clipGroupConfig">
|
||||
@@ -1171,6 +1474,7 @@ onBeforeUnmount(() => {
|
||||
<v-group
|
||||
v-if="pivotHandleConfig"
|
||||
:config="pivotHandleConfig"
|
||||
@dragstart="onPivotDragStart"
|
||||
@dragend="onPivotDragEnd"
|
||||
>
|
||||
<v-circle :config="pivotRingConfig" />
|
||||
@@ -1191,6 +1495,12 @@ onBeforeUnmount(() => {
|
||||
@dragmove="(e: Konva.KonvaEventObject<DragEvent>) => onVertexDragMove(e, h.index)"
|
||||
@dragend="(e: Konva.KonvaEventObject<DragEvent>) => onVertexDragEnd(e, h.index)"
|
||||
/>
|
||||
<v-line v-if="drawingPreviewConfig" :config="drawingPreviewConfig" />
|
||||
<v-circle
|
||||
v-for="d in drawingDotsConfig"
|
||||
:key="d.key"
|
||||
:config="d"
|
||||
/>
|
||||
</v-group>
|
||||
<v-transformer ref="transformerRef" :config="transformerConfig" />
|
||||
</v-layer>
|
||||
@@ -1244,6 +1554,10 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stage-container.drawing {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.mask-preview-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Reference in new issue
Block a user