diff --git a/VERSION b/VERSION
index 5d4294b..2411653 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.5.1
\ No newline at end of file
+0.5.2
\ No newline at end of file
diff --git a/src/components/elements/groupobject.js b/src/components/elements/groupobject.js
index 4ac163b..801c2a7 100644
--- a/src/components/elements/groupobject.js
+++ b/src/components/elements/groupobject.js
@@ -119,8 +119,9 @@ function GroupedObjectWrapper(props) {
if (element.elementData.componentType === 'pencil') {
newMetadata = element.elementData.metadata.map(
(vert, index) => {
+ const lwProp = vert.lw !== undefined ? { lw: vert.lw } : {}
if (index === 0) {
- return { x: newX, y: newY }
+ return { x: newX, y: newY, ...lwProp }
} else if (index > 0) {
// here the logic is to get relative vertex coordinates to the original metadata
// so we want to get result of ( relative coordinate + orginal_vert(x) - originalX )
@@ -141,6 +142,7 @@ function GroupedObjectWrapper(props) {
element.elementData
.metadata[0].y
),
+ ...lwProp,
}
}
}
diff --git a/src/components/elements/pencil.js b/src/components/elements/pencil.js
index c37e805..f657a4a 100644
--- a/src/components/elements/pencil.js
+++ b/src/components/elements/pencil.js
@@ -59,15 +59,16 @@ function Pencil(props) {
})
// Get all instances of every sub child element
const { group, path } = elementFactory.createElement()
+ const shapeRef = path || group.children[0]
group.elementData = { ...props.itemData, ...props }
if (props.parentGroup) {
/** This element will be rendered and scoped in its parent group */
console.log('properties of pencil', props)
const parentGroup = props.parentGroup
- path.translation.x = props.properties.x
- path.translation.y = props.properties.y
- parentGroup.add(path)
+ shapeRef.translation.x = props.properties.x
+ shapeRef.translation.y = props.properties.y
+ parentGroup.add(shapeRef)
two.update()
} else {
/** This element will render by creating it's own group wrapper */
@@ -76,7 +77,9 @@ function Pencil(props) {
const { selector } = getEditComponents(two, group, 4)
selectorInstance = selector
- group.children.unshift(path)
+ if (path) {
+ group.children.unshift(path)
+ }
two.update()
// document
@@ -100,7 +103,7 @@ function Pencil(props) {
setInternalState((draft) => {
draft.element = {
- [path.id]: path,
+ [shapeRef.id]: shapeRef,
[group.id]: group,
// [selector.id]: selector,
}
@@ -109,8 +112,8 @@ function Pencil(props) {
data: group,
}
draft.shape = {
- id: path.id,
- data: path,
+ id: shapeRef.id,
+ data: shapeRef,
}
draft.text = {
data: {},
diff --git a/src/factory/pencil.js b/src/factory/pencil.js
index 7cd917a..a959aaa 100644
--- a/src/factory/pencil.js
+++ b/src/factory/pencil.js
@@ -1,6 +1,6 @@
import Main from './main'
-import { color_blue } from 'utils/constants'
import Two from 'two.js'
+import { mergeSegmentsByLinewidth, averageLinewidth } from 'utils/pencilHelper'
export default class PencilFactory extends Main {
createElement() {
@@ -10,7 +10,37 @@ export default class PencilFactory extends Main {
const { fill, width, height, radius, stroke, linewidth, metadata } =
this.properties
- // let paths = []
+ const hasLwData = metadata.length > 0 && metadata[0].lw !== undefined
+
+ if (hasLwData) {
+ // New format: multi-path rendering with variable linewidth
+ const segments = mergeSegmentsByLinewidth(metadata)
+ const group = two.makeGroup()
+
+ segments.forEach((segmentPoints) => {
+ if (segmentPoints.length < 2) return
+ const path = two.makePath()
+ segmentPoints.forEach((point) => {
+ path.vertices.push(
+ new Two.Vector(point.x - prevX, point.y - prevY)
+ )
+ })
+ path.noFill()
+ path.stroke = stroke || '#000'
+ path.closed = false
+ path.cap = 'round'
+ path.join = 'round'
+ path.linewidth = averageLinewidth(segmentPoints)
+ group.add(path)
+ })
+
+ group.translation.x = parseInt(prevX)
+ group.translation.y = parseInt(prevY)
+ this.group = group
+ return { group: this.group }
+ }
+
+ // Legacy format: single path with uniform linewidth (backward compatible)
let path = two.makePath()
if (metadata.length > 0) {
metadata.forEach(function (point) {
@@ -21,21 +51,15 @@ export default class PencilFactory extends Main {
path.noFill()
path.stroke = '#000'
path.closed = false
- // two.add(path)
- // paths.push(path)
}
- // path.fill = fill ? fill : color_blue
-
path.linewidth = linewidth ? linewidth : 1
this.path = path
- // Create group and take children elements as a parameter
const group = two.makeGroup(path)
group.translation.x = parseInt(prevX)
group.translation.y = parseInt(prevY)
this.group = group
- console.log('group.id pencil', group.id)
return { group: this.group, path }
}
}
diff --git a/src/newCanvas.js b/src/newCanvas.js
index 8dac2fc..0ddec48 100644
--- a/src/newCanvas.js
+++ b/src/newCanvas.js
@@ -13,6 +13,11 @@ import Spinner from 'components/common/spinner'
import Loader from 'components/utils/loader'
import { updateX1Y1Vertices, updateX2Y2Vertices } from 'utils/updateVertices'
import { generateUUID } from 'utils/misc'
+import {
+ velocityToLinewidth,
+ smoothLinewidth,
+ simplifyWithLinewidth,
+} from 'utils/pencilHelper'
function getComponentSchema(obj, boardId) {
let generateId = generateUUID()
@@ -59,6 +64,7 @@ function getComponentSchema(obj, boardId) {
var isDrawing
var defaultLinewidthValue = 1
+var pencilStrokeColorValue = '#000'
function addZUI(
props,
@@ -86,6 +92,13 @@ function addZUI(
let lastAddedPath
let paths = []
+ // Velocity-based pencil state
+ let pencilGroup = null
+ let pencilRawPoints = []
+ let lastPencilPoint = null
+ let lastPencilTime = 0
+ let lastPencilLinewidth = null
+
let scenario = null
let SCENARIO_JUST_ADDED_ELEMENT = 'justAddedElement'
let SCENARIO_PENCIL_MODE = 'pencilMode'
@@ -304,17 +317,25 @@ function addZUI(
document.getElementById('main-two-root').style.cursor = 'auto'
break
- case SCENARIO_PENCIL_MODE:
- // do here
+ case SCENARIO_PENCIL_MODE: {
domElement.addEventListener('mousemove', mousemove, false)
domElement.addEventListener('mouseup', mouseup, false)
- currentPath = two.makePath()
- currentPath.linewidth = defaultLinewidthValue
- currentPath.closed = false
- two.add(currentPath)
- paths.push(currentPath)
+ // Create a group for live preview segments
+ pencilGroup = two.makeGroup()
+ pencilRawPoints = []
+ lastPencilLinewidth = defaultLinewidthValue
+
+ const startCoords = zui.clientToSurface(e.clientX, e.clientY)
+ lastPencilPoint = { x: startCoords.x, y: startCoords.y }
+ lastPencilTime = performance.now()
+ pencilRawPoints.push({
+ x: startCoords.x,
+ y: startCoords.y,
+ lw: lastPencilLinewidth,
+ })
break
+ }
default:
shape = null
mouse.x = e.clientX
@@ -667,19 +688,49 @@ function addZUI(
two.update()
}
break
- case SCENARIO_PENCIL_MODE:
- let getCoordinate = zui.clientToSurface(e.clientX, e.clientY)
-
- currentPath.vertices.push(
- new Two.Vector(getCoordinate.x, getCoordinate.y)
+ case SCENARIO_PENCIL_MODE: {
+ const pencilCoords = zui.clientToSurface(e.clientX, e.clientY)
+
+ // Distance throttle: skip if too close to last point
+ const pdx = pencilCoords.x - lastPencilPoint.x
+ const pdy = pencilCoords.y - lastPencilPoint.y
+ const pDist = Math.sqrt(pdx * pdx + pdy * pdy)
+ if (pDist < 3) break
+
+ // Compute velocity and map to linewidth
+ const now = performance.now()
+ const timeDelta = now - lastPencilTime
+ const velocity = timeDelta > 0 ? pDist / timeDelta : 0
+ const targetLw = velocityToLinewidth(velocity, defaultLinewidthValue)
+ const smoothedLw = smoothLinewidth(lastPencilLinewidth, targetLw, 0.3)
+
+ // Create a 2-point path segment for live preview
+ const segment = two.makePath(
+ lastPencilPoint.x, lastPencilPoint.y,
+ pencilCoords.x, pencilCoords.y
)
- currentPath.vertices[currentPath.vertices.length - 1].command =
- 'L'
- currentPath.noFill()
- currentPath.stroke = '#000'
- currentPath.linewidth = defaultLinewidthValue
+ segment.noFill()
+ segment.stroke = pencilStrokeColorValue
+ segment.linewidth = smoothedLw
+ segment.cap = 'round'
+ segment.join = 'round'
+ segment.closed = false
+ pencilGroup.add(segment)
+
+ // Record point with linewidth
+ pencilRawPoints.push({
+ x: pencilCoords.x,
+ y: pencilCoords.y,
+ lw: smoothedLw,
+ })
+
+ lastPencilPoint = { x: pencilCoords.x, y: pencilCoords.y }
+ lastPencilTime = now
+ lastPencilLinewidth = smoothedLw
+
two.update()
break
+ }
default:
/**
Currently "resize" event handling is at component level.
@@ -1056,48 +1107,53 @@ function addZUI(
domElement.removeEventListener('mousemove', mousemove, false)
domElement.removeEventListener('mouseup', mouseup, false)
break
- case SCENARIO_PENCIL_MODE:
- // isDrawing = false
- // console.log(
- // 'on mouse up pencil mode',
- // paths,
- // currentPath.vertices,
- // currentPath.translation
- // )
-
- let generateId = generateUUID()
- let componentData = {
- id: generateId,
+ case SCENARIO_PENCIL_MODE: {
+ // Remove live preview group — React component will re-render from metadata
+ if (pencilGroup) {
+ two.remove(pencilGroup)
+ }
+
+ if (pencilRawPoints.length < 2) {
+ // Too few points to form a stroke
+ pencilGroup = null
+ pencilRawPoints = []
+ lastPencilPoint = null
+ lastPencilLinewidth = null
+ break
+ }
+
+ // Simplify points while preserving lw
+ const simplifiedPoints = simplifyWithLinewidth(pencilRawPoints, 1.5)
+
+ let pencilId = generateUUID()
+ let pencilComponentData = {
+ id: pencilId,
boardId: props.boardId,
componentType: 'pencil',
children: {},
- metadata: [],
+ metadata: simplifiedPoints,
x: 0,
y: 0,
- linewidth: currentPath.linewidth,
- stroke: currentPath.stroke,
+ linewidth: defaultLinewidthValue,
+ stroke: pencilStrokeColorValue,
}
- componentData.metadata = currentPath.vertices.map(
- function (vertex) {
- return { x: vertex.x, y: vertex.y }
- }
- )
- componentData.x = Math.floor(componentData.metadata[0]?.x || 0)
- componentData.y = Math.floor(componentData.metadata[0]?.y || 0)
+ pencilComponentData.x = Math.floor(simplifiedPoints[0]?.x || 0)
+ pencilComponentData.y = Math.floor(simplifiedPoints[0]?.y || 0)
addToLocalComponentStore(
- componentData.id,
- componentData.componentType,
- componentData
+ pencilComponentData.id,
+ pencilComponentData.componentType,
+ pencilComponentData
)
- // let currentPathRef = currentPath
- // setTimeout(() => {
- // two.remove(currentPathRef)
- // two.update()
- // }, 5000)
+ // Reset pencil state
+ pencilGroup = null
+ pencilRawPoints = []
+ lastPencilPoint = null
+ lastPencilLinewidth = null
break
+ }
default:
// diff to check new x,y and prev x,y
let oldShapeData = {}
@@ -1496,6 +1552,10 @@ const Canvas = (props) => {
defaultLinewidthValue = props.defaultLinewidth || 1
}, [props.defaultLinewidth])
+ useEffect(() => {
+ pencilStrokeColorValue = props.pencilStrokeColor || '#000'
+ }, [props.pencilStrokeColor])
+
// on group select use effect hook
useEffect(() => {
// let componentsArr = [...currentComponents]
@@ -1546,8 +1606,9 @@ const Canvas = (props) => {
let newMetadata = []
if (item.componentType === 'pencil') {
newMetadata = item.metadata.map((vert, index) => {
+ const lwProp = vert.lw !== undefined ? { lw: vert.lw } : {}
if (index === 0) {
- return { x: relativeX, y: relativeY }
+ return { x: relativeX, y: relativeY, ...lwProp }
} else if (index > 0) {
// here the logic is to get relative vertex coordinates to the original metadata
// so we want to get result of ( relative coordinate + orginal_vert(x) - originalX )
@@ -1560,6 +1621,7 @@ const Canvas = (props) => {
y:
relativeY +
parseInt(vert.y - item.metadata[0].y),
+ ...lwProp,
}
}
})
diff --git a/src/utils/pencilHelper.js b/src/utils/pencilHelper.js
new file mode 100644
index 0000000..c2b1b13
--- /dev/null
+++ b/src/utils/pencilHelper.js
@@ -0,0 +1,141 @@
+/**
+ * Pencil helper utilities for velocity-based variable stroke width.
+ *
+ * Slow drawing = thick lines, fast drawing = thin lines.
+ */
+
+// Velocity thresholds (px/ms)
+const SLOW_VELOCITY = 0.1
+const FAST_VELOCITY = 1.5
+
+/**
+ * Maps drawing velocity (px/ms) to a linewidth.
+ * Slow = thick (baseWidth * 2), fast = thin (baseWidth * 0.5).
+ */
+export function velocityToLinewidth(velocity, baseWidth) {
+ const maxWidth = baseWidth * 2.5
+ const minWidth = baseWidth * 0.5
+
+ // Clamp velocity to range
+ const clampedVelocity = Math.max(
+ SLOW_VELOCITY,
+ Math.min(FAST_VELOCITY, velocity)
+ )
+
+ // Linear interpolation: slow -> maxWidth, fast -> minWidth
+ const t =
+ (clampedVelocity - SLOW_VELOCITY) / (FAST_VELOCITY - SLOW_VELOCITY)
+ return maxWidth - t * (maxWidth - minWidth)
+}
+
+/**
+ * Exponential moving average to smooth linewidth transitions.
+ */
+export function smoothLinewidth(prevWidth, targetWidth, factor = 0.3) {
+ return prevWidth + factor * (targetWidth - prevWidth)
+}
+
+/**
+ * Perpendicular distance from a point to a line segment (for RDP algorithm).
+ */
+function perpendicularDistance(point, lineStart, lineEnd) {
+ const dx = lineEnd.x - lineStart.x
+ const dy = lineEnd.y - lineStart.y
+
+ if (dx === 0 && dy === 0) {
+ return Math.sqrt(
+ (point.x - lineStart.x) ** 2 + (point.y - lineStart.y) ** 2
+ )
+ }
+
+ const t =
+ ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
+ (dx * dx + dy * dy)
+ const clampedT = Math.max(0, Math.min(1, t))
+ const projX = lineStart.x + clampedT * dx
+ const projY = lineStart.y + clampedT * dy
+
+ return Math.sqrt((point.x - projX) ** 2 + (point.y - projY) ** 2)
+}
+
+/**
+ * Ramer-Douglas-Peucker simplification that preserves {x, y, lw} tuples.
+ */
+export function simplifyWithLinewidth(points, epsilon) {
+ if (points.length <= 2) return points
+
+ let maxDist = 0
+ let maxIndex = 0
+
+ for (let i = 1; i < points.length - 1; i++) {
+ const dist = perpendicularDistance(
+ points[i],
+ points[0],
+ points[points.length - 1]
+ )
+ if (dist > maxDist) {
+ maxDist = dist
+ maxIndex = i
+ }
+ }
+
+ if (maxDist > epsilon) {
+ const left = simplifyWithLinewidth(
+ points.slice(0, maxIndex + 1),
+ epsilon
+ )
+ const right = simplifyWithLinewidth(points.slice(maxIndex), epsilon)
+ return left.slice(0, -1).concat(right)
+ }
+
+ return [points[0], points[points.length - 1]]
+}
+
+/**
+ * Groups consecutive points with similar linewidths into longer path segments.
+ * Adjacent groups share an overlap point to prevent visual gaps.
+ *
+ * Returns array of point arrays, each representing one path segment.
+ */
+export function mergeSegmentsByLinewidth(points, lwTolerance = 0.3) {
+ if (points.length <= 1) return [points]
+
+ const segments = []
+ let currentSegment = [points[0]]
+
+ for (let i = 1; i < points.length; i++) {
+ const prevLw = currentSegment[currentSegment.length - 1].lw || 1
+ const currLw = points[i].lw || 1
+
+ if (
+ Math.abs(currLw - prevLw) / Math.max(prevLw, currLw) <=
+ lwTolerance
+ ) {
+ // Similar enough linewidth, keep in same segment
+ currentSegment.push(points[i])
+ } else {
+ // Linewidth difference too large, start new segment
+ // Share overlap point to prevent gaps
+ segments.push(currentSegment)
+ currentSegment = [
+ currentSegment[currentSegment.length - 1],
+ points[i],
+ ]
+ }
+ }
+
+ if (currentSegment.length > 0) {
+ segments.push(currentSegment)
+ }
+
+ return segments
+}
+
+/**
+ * Computes the average linewidth for a segment of points.
+ */
+export function averageLinewidth(points) {
+ if (points.length === 0) return 1
+ const sum = points.reduce((acc, p) => acc + (p.lw || 1), 0)
+ return sum / points.length
+}
diff --git a/src/views/Board/board.js b/src/views/Board/board.js
index ace462d..0927f8d 100644
--- a/src/views/Board/board.js
+++ b/src/views/Board/board.js
@@ -21,6 +21,7 @@ import Canvas from '../../newCanvas'
import Sidebar from 'components/sidebar/primary'
import Toolbar from 'components/floatingToolbar'
import Spinner from 'components/common/spinnerWithSize'
+import ColorPicker from 'components/utils/colorPicker'
import { generateRandomUsernames } from 'utils/misc'
const BoardContext = createContext()
@@ -78,10 +79,7 @@ const BoardViewPage = (props) => {
const [
createBoard,
- {
- loading: createBoardLoading,
- data: createBoardData,
- },
+ { loading: createBoardLoading, data: createBoardData },
] = useMutation(CREATE_BOARD)
const [componentStore, setComponentStore] = useState({})
@@ -94,6 +92,7 @@ const BoardViewPage = (props) => {
const [twoJSInstance, setTwoJSInstance] = useState(null)
const [selectedComponent, setSelectedComponent] = useState(null)
const [defaultLinewidth, setDefaultLinewidth] = useState(2)
+ const [pencilStrokeColor, setPencilStrokeColor] = useState('#000')
const [currentElement, setCurrentElement] = useState(null)
const { isDesktop, isMobile, isLaptop, isTablet } = useMediaQueryUtils()
@@ -336,6 +335,10 @@ const BoardViewPage = (props) => {
setDefaultLinewidth(val)
}
+ const setPencilStrokeColorInBoard = (val) => {
+ setPencilStrokeColor(val)
+ }
+
const setCurrentElementInBoard = (val) => {
setCurrentElement(val)
}
@@ -363,6 +366,26 @@ const BoardViewPage = (props) => {
})
}
+ const renderBorderWidths = () => {
+ const widths = [0, 2, 4, 8].map((width, index) => {
+ return (
+