Skip to content

Commit 4ef5e11

Browse files
nmndwivediclaude
andcommitted
v0.1.1 — bendable lines (midpoint anchor), fix ungrabbable bend handle on straight arrows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1bc5c36 commit 4ef5e11

13 files changed

Lines changed: 80 additions & 43 deletions

File tree

‎.claude/launch.json‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,50 @@
44
{
55
"name": "react-demo",
66
"runtimeExecutable": "npm",
7-
"runtimeArgs": ["run", "dev", "--workspace=quickdraw-react-demo", "--", "--port", "5199", "--strictPort"],
8-
"port": 5199
7+
"runtimeArgs": [
8+
"run",
9+
"dev",
10+
"--workspace=quickdraw-react-demo"
11+
],
12+
"port": 5199,
13+
"autoPort": true
914
},
1015
{
1116
"name": "website",
1217
"runtimeExecutable": "npm",
13-
"runtimeArgs": ["run", "dev", "--workspace=quickdraw-website", "--", "--port", "4399"],
18+
"runtimeArgs": [
19+
"run",
20+
"dev",
21+
"--workspace=quickdraw-website",
22+
"--",
23+
"--port",
24+
"4399"
25+
],
1426
"port": 4399
1527
},
1628
{
1729
"name": "app",
1830
"runtimeExecutable": "npm",
19-
"runtimeArgs": ["run", "dev", "--workspace=quickdraw-app", "--", "--port", "5299", "--strictPort"],
31+
"runtimeArgs": [
32+
"run",
33+
"dev",
34+
"--workspace=quickdraw-app",
35+
"--",
36+
"--port",
37+
"5299",
38+
"--strictPort"
39+
],
2040
"port": 5299
2141
},
2242
{
2343
"name": "docs",
2444
"runtimeExecutable": "npm",
25-
"runtimeArgs": ["run", "dev", "--workspace=quickdraw-docs"],
45+
"runtimeArgs": [
46+
"run",
47+
"dev",
48+
"--workspace=quickdraw-docs"
49+
],
2650
"port": 4499
2751
}
2852
]
29-
}
53+
}

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to the `@quickdrawjs/*` packages are documented here.
44
The project follows [semver](https://semver.org); the three packages are
55
versioned in lockstep.
66

7+
## 0.1.1 — 2026-08-01
8+
9+
- Lines are now bendable: drag the new midpoint anchor to curve a line,
10+
exactly like arrows. Lines and arrows both show three anchors
11+
(start, midpoint, end).
12+
- Fixed: straight arrows could not be bent — the midpoint anchor
13+
collapsed onto the end anchor when `bend` was 0, making it ungrabbable.
14+
715
## 0.1.0 — 2026-08-01
816

917
First public release.

‎apps/app/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
"name": "quickdraw-app",
33
"private": true,
44
"version": "0.0.0",
5-
"description": "app.tryquickdraw.com — the hosted Quickdraw whiteboard.",
5+
"description": "app.tryquickdraw.com \u2014 the hosted Quickdraw whiteboard.",
66
"type": "module",
77
"scripts": {
88
"dev": "vite",
99
"build": "vite build",
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@quickdrawjs/core": "0.1.0"
13+
"@quickdrawjs/core": "0.1.1"
1414
},
1515
"devDependencies": {
1616
"vite": "^6.3.0"

‎apps/website/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "quickdraw-website",
33
"private": true,
44
"version": "0.0.0",
5-
"description": "tryquickdraw.com — the Quickdraw marketing site and blog.",
5+
"description": "tryquickdraw.com \u2014 the Quickdraw marketing site and blog.",
66
"type": "module",
77
"scripts": {
88
"dev": "astro dev",
@@ -11,7 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@astrojs/sitemap": "^3.3.0",
14-
"@quickdrawjs/core": "0.1.0",
14+
"@quickdrawjs/core": "0.1.1",
1515
"astro": "^5.7.0"
1616
}
1717
}

‎examples/react-demo/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12-
"@quickdrawjs/react": "0.1.0",
12+
"@quickdrawjs/react": "0.1.1",
1313
"react": "^19.1.0",
1414
"react-dom": "^19.1.0"
1515
},

‎examples/react-demo/vite.config.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import react from '@vitejs/plugin-react'
33

44
export default defineConfig({
55
plugins: [react()],
6+
server: process.env.PORT ? { port: Number(process.env.PORT), strictPort: true } : {},
67
})

‎package-lock.json‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/core/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@quickdrawjs/core",
3-
"version": "0.1.0",
4-
"description": "Framework-free infinite-canvas whiteboard engine — the heart of Quickdraw, the MIT-licensed drawing SDK.",
3+
"version": "0.1.1",
4+
"description": "Framework-free infinite-canvas whiteboard engine \u2014 the heart of Quickdraw, the MIT-licensed drawing SDK.",
55
"type": "module",
66
"main": "./src/index.js",
77
"module": "./src/index.js",

‎packages/core/src/editor.js‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ const DEFAULT_STYLES = { color: 'black', size: 'm', dash: 'draw', fill: 'none',
2222

2323
export const TOOLS = ['select', 'hand', 'draw', 'highlight', 'eraser', 'laser', 'arrow', 'line', 'geo', 'text', 'note']
2424

25+
// local position of the bend handle: the curve's midpoint (chord midpoint
26+
// when straight — sampleLinePts collapses to the two endpoints at bend 0)
27+
const bendMidpoint = (pr) => {
28+
if (!pr.bend) return { x: pr.dx / 2, y: pr.dy / 2 }
29+
const mid = sampleLinePts(pr, pr.bend)
30+
const mi = Math.floor(mid.length / 4) * 2
31+
return { x: mid[mi], y: mid[mi + 1] }
32+
}
33+
2534
export class Editor {
2635
constructor({ container, store, theme = 'light', grid = 'none', readonly = false, camera, styles, geoKind } = {}) {
2736
this.container = container
@@ -772,7 +781,7 @@ export class Editor {
772781
this.store.put({
773782
id, typeName: 'shape', type, x: p.x, y: p.y, rot: 0, z: this.store.maxZ() + 1,
774783
props: {
775-
dx: 0.01, dy: 0.01, ...(type === 'arrow' ? { bend: 0 } : {}),
784+
dx: 0.01, dy: 0.01, bend: 0,
776785
color: this.styles.color, size: this.styles.size,
777786
dash: this.styles.dash === 'draw' ? 'solid' : this.styles.dash,
778787
},
@@ -1146,11 +1155,8 @@ export class Editor {
11461155
{ which: 'start', x: one.x, y: one.y },
11471156
{ which: 'end', x: one.x + pr.dx, y: one.y + pr.dy },
11481157
]
1149-
if (one.type === 'arrow') {
1150-
const mid = sampleLinePts(pr, pr.bend || 0)
1151-
const mi = Math.floor(mid.length / 4) * 2
1152-
pts.push({ which: 'bend', x: one.x + mid[mi], y: one.y + mid[mi + 1] })
1153-
}
1158+
const bm = bendMidpoint(pr)
1159+
pts.push({ which: 'bend', x: one.x + bm.x, y: one.y + bm.y })
11541160
for (const h of pts) {
11551161
const s = this.pageToScreen(h.x, h.y)
11561162
if (Math.hypot(s.x - sx, s.y - sy) <= HANDLE + 3) return { kind: 'handle', which: h.which, id: one.id }
@@ -1601,11 +1607,8 @@ export class Editor {
16011607
this.pageToScreen(one.x, one.y),
16021608
this.pageToScreen(one.x + pr.dx, one.y + pr.dy),
16031609
]
1604-
if (one.type === 'arrow') {
1605-
const mid = sampleLinePts(pr, pr.bend || 0)
1606-
const mi = Math.floor(mid.length / 4) * 2
1607-
hs.push(this.pageToScreen(one.x + mid[mi], one.y + mid[mi + 1]))
1608-
}
1610+
const bm = bendMidpoint(pr)
1611+
hs.push(this.pageToScreen(one.x + bm.x, one.y + bm.y))
16091612
for (const p2 of hs) {
16101613
ctx.beginPath()
16111614
ctx.arc(p2.x, p2.y, 5, 0, Math.PI * 2)

‎packages/core/src/shapes.js‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function localBounds(shape) {
3535
}
3636
case 'arrow':
3737
case 'line': {
38-
const bend = shape.type === 'arrow' ? p.bend || 0 : 0
38+
const bend = p.bend || 0
3939
const x = Math.min(0, p.dx) - Math.abs(bend)
4040
const y = Math.min(0, p.dy) - Math.abs(bend)
4141
return { x, y, w: Math.abs(p.dx) + Math.abs(bend) * 2, h: Math.abs(p.dy) + Math.abs(bend) * 2 }
@@ -378,7 +378,7 @@ export function drawShape(ctx, shape, opts) {
378378
const w = SIZES[p.size]
379379
ctx.strokeStyle = col.stroke
380380
strokeStyled(ctx, p.dash, w)
381-
const bend = shape.type === 'arrow' ? p.bend || 0 : 0
381+
const bend = p.bend || 0
382382
const len = Math.hypot(p.dx, p.dy) || 1
383383
const nx = -p.dy / len, ny = p.dx / len
384384
const cx2 = p.dx / 2 + nx * bend * 2 // control point: bend*2 puts the CURVE at bend offset
@@ -499,8 +499,7 @@ export function hitShape(shape, px, py, tol, store) {
499499
}
500500
case 'arrow':
501501
case 'line': {
502-
const bend = shape.type === 'arrow' ? p.bend || 0 : 0
503-
const pts = sampleLinePts(p, bend)
502+
const pts = sampleLinePts(p, p.bend || 0)
504503
return distToPolyline(l.x, l.y, pts, 2) <= tol + SIZES[p.size]
505504
}
506505
case 'text':
@@ -549,7 +548,7 @@ export function marqueeHits(shape, rect) {
549548
const local = { x: rect.x - shape.x, y: rect.y - shape.y, w: rect.w, h: rect.h }
550549
let pts, stride = 2
551550
if (shape.type === 'draw' || shape.type === 'highlight') { pts = p.pts; stride = 3 }
552-
else if (shape.type === 'arrow' || shape.type === 'line') pts = sampleLinePts(p, shape.type === 'arrow' ? p.bend || 0 : 0)
551+
else if (shape.type === 'arrow' || shape.type === 'line') pts = sampleLinePts(p, p.bend || 0)
553552
else if (shape.type === 'geo') { pts = geoPolygon(p.geo, p.w, p.h); if (pointInPolygon(local.x + local.w / 2, local.y + local.h / 2, pts)) return true }
554553
if (!pts) return true
555554
const n = Math.floor(pts.length / stride)

0 commit comments

Comments
 (0)