diff --git a/public/low-wireframes/textScribbled.svg b/public/low-wireframes/textScribbled.svg
new file mode 100644
index 00000000..5418762d
--- /dev/null
+++ b/public/low-wireframes/textScribbled.svg
@@ -0,0 +1,28 @@
+
diff --git a/src/common/components/mock-components/front-low-wireframes-components/index.ts b/src/common/components/mock-components/front-low-wireframes-components/index.ts
index 3de0c0f5..88e33897 100644
--- a/src/common/components/mock-components/front-low-wireframes-components/index.ts
+++ b/src/common/components/mock-components/front-low-wireframes-components/index.ts
@@ -4,3 +4,4 @@ export * from './image-placeholder-shape';
export * from './vertical-line-low-shape';
export * from './rectangle-low-shape';
export * from './circle-low-shape';
+export * from './text-scribbled-shape/text-scribbled-shape';
diff --git a/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled-shape.tsx b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled-shape.tsx
new file mode 100644
index 00000000..93ac878c
--- /dev/null
+++ b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled-shape.tsx
@@ -0,0 +1,66 @@
+import { forwardRef, useMemo } from 'react';
+import { Group, Path, Rect } from 'react-konva';
+import { ShapeSizeRestrictions, ShapeType } from '@/core/model';
+import { ShapeProps } from '../../shape.model';
+import { useShapeProps } from '../../../shapes/use-shape-props.hook';
+import { BASIC_SHAPE } from '../../front-components/shape.const';
+import { useGroupShapeProps } from '../../mock-components.utils';
+import { calculatePath } from './text-scribbled.business';
+import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes';
+
+const textScribbledShapeRestrictions: ShapeSizeRestrictions = {
+ minWidth: 100,
+ minHeight: 45,
+ maxWidth: -1,
+ maxHeight: -1,
+ defaultWidth: 300,
+ defaultHeight: 50,
+};
+
+export const getTextScribbledShapeRestrictions = (): ShapeSizeRestrictions =>
+ textScribbledShapeRestrictions;
+
+const shapeType: ShapeType = 'textScribbled';
+
+export const TextScribbled = forwardRef((props, ref) => {
+ const { width, height, id, otherProps, ...shapeProps } = props;
+
+ const { stroke } = useShapeProps(otherProps, BASIC_SHAPE);
+ const commonGroupProps = useGroupShapeProps(
+ props,
+ { width, height },
+ shapeType,
+ ref
+ );
+
+ const restrictedSize = fitSizeToShapeSizeRestrictions(
+ textScribbledShapeRestrictions,
+ width,
+ height
+ );
+
+ const { width: restrictedWidth, height: restrictedHeight } = restrictedSize;
+
+ const pathData = useMemo(() => {
+ return calculatePath(restrictedWidth, restrictedHeight, id);
+ }, [restrictedWidth]);
+
+ return (
+
+
+ {/* Had to add a rectangle to allow drag / drop movement*/}
+
+
+ );
+});
diff --git a/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.business.ts b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.business.ts
new file mode 100644
index 00000000..601d7ecb
--- /dev/null
+++ b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.business.ts
@@ -0,0 +1,144 @@
+import {
+ AVG_CHAR_WIDTH,
+ SEED_PHRASE,
+ SPACE_WIDTH,
+} from './text-scribbled.const';
+
+export const seededRandom = (seed: number) => {
+ // Let's get a random value in between -1 and 1
+ // And let's multiply it by 10000 to get a bigger number (more precision)
+ const x = Math.sin(seed) * 10000;
+
+ // Le's extract the decimal part of the number
+ // a number in between 0 and 1
+ return x - Math.floor(x);
+};
+
+// 30 characters is enough to get a good random offset phrase[X]
+// in the past it was phrase.length, but that can lead to issues
+// if the offset start at the end of the phrase then we can get a frozen text when we make it bigger.
+const MAX_START_OFFSET = 30;
+
+// We need to add some random offset to start the text at a different position
+// BUT we cannot use here just a random number because it will change every time
+// the component is re-rendered, so we need to use a deterministic way to get the offset
+// based on the Id of the shape
+// 👇 Based on the Id deterministic offset
+// a bit weird, maybe just a random useEffect []
+export const getOffsetFromId = (id: string, max: number) => {
+ let sum = 0;
+ for (let i = 0; i < id.length; i++) {
+ sum += id.charCodeAt(i);
+ }
+ return sum % max;
+};
+
+export const rounded = (value: number) => Math.round(value * 2) / 2;
+
+export const addBlankSpaceToPath = (
+ currentX: number,
+ maxWidth: number,
+ height: number
+) => {
+ currentX += SPACE_WIDTH;
+
+ // We don't want to go out of the area, if not transformer won't work well
+ const adjustedEndX = Math.min(currentX, maxWidth - 1);
+
+ return {
+ pathSlice: `M ${adjustedEndX},${Math.trunc(height / 2)}`,
+ newCurrentX: currentX,
+ };
+};
+
+const drawCharScribble = (
+ char: string,
+ i: number,
+ currentX: number,
+ maxWidth: number,
+ height: number
+) => {
+ // Max Y variation on the scribble
+ const amplitude = height / 3;
+ const charWidth = AVG_CHAR_WIDTH;
+ // Let's generate a psuedo-random number based on the char and the index
+ const seed = char.charCodeAt(0) + i * 31;
+
+ const controlX1 = currentX + charWidth / 2;
+ const controlY1 = Math.trunc(
+ rounded(
+ // Generate a pseudo random number between -amplitude and amplitude
+ height / 2 + (seededRandom(seed) * amplitude - amplitude / 2)
+ )
+ );
+
+ const controlX2 = currentX + charWidth;
+ const controlY2 = Math.trunc(
+ rounded(height / 2 + (seededRandom(seed + 1) * amplitude - amplitude / 2))
+ );
+
+ // Let's truc it to avoid edge cases with the max
+ const endX = Math.trunc(currentX + charWidth);
+ const endY = Math.trunc(height / 2);
+
+ // We don't want to go out of the area, if not transformer won't work well
+ const adjustedEndX = Math.min(endX, maxWidth - 1);
+
+ return {
+ pathSegment: `C ${controlX1},${controlY1} ${controlX2},${controlY2} ${adjustedEndX},${endY}`,
+ endX,
+ };
+};
+
+export const calculatePath = (width: number, height: number, id: string) => {
+ //console.log('** calculatePath', width, height, id);
+ // This AVG_CHAR_WIDTH is a rough approximation of the average character width
+ // It could lead us to issues
+ const offset = getOffsetFromId(id ?? '', MAX_START_OFFSET);
+
+ // In the past it was: /*offset + maxChars*/
+ // but just updated to SEED_PHRASE.length to ensure we have enough cahrs despite
+ // the average offset (the loop will break if we run out of space)
+ const visibleText = SEED_PHRASE.slice(offset, SEED_PHRASE.length);
+
+ const path: string[] = [];
+ let currentX = 0;
+ path.push(`M ${currentX},${Math.trunc(height / 2)}`);
+
+ for (let i = 0; i < visibleText.length; i++) {
+ const char = visibleText[i];
+
+ if (char !== ' ') {
+ // Draw the character scribble
+ const { pathSegment, endX } = drawCharScribble(
+ char,
+ i,
+ currentX,
+ width,
+ height
+ );
+ path.push(pathSegment);
+ currentX = endX;
+ } else {
+ // If it's a space, we need to add a blank space to the path
+ const { pathSlice, newCurrentX } = addBlankSpaceToPath(
+ currentX,
+ width,
+ height
+ );
+ path.push(pathSlice);
+ currentX = newCurrentX;
+ }
+
+ // If we run out of space, we break the loop
+ // We need to add the AVG_CHAR_WIDTH to the equation to avoid
+ // rending a scribble that could be outside of the area
+ // and make the transformer not work well
+ if (currentX + AVG_CHAR_WIDTH >= width) break;
+ }
+
+ const result = path.join(' ');
+ console.log('** calculatePath result', result);
+
+ return path.join(' ');
+};
diff --git a/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.const.ts b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.const.ts
new file mode 100644
index 00000000..8e0157dc
--- /dev/null
+++ b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.const.ts
@@ -0,0 +1,29 @@
+// We setup an AVG CHAR_WIDTH to avoid using the getTextWidth function
+export const AVG_CHAR_WIDTH = 10;
+
+// Blank space width is 1.5 times the average character width
+export const SPACE_WIDTH = AVG_CHAR_WIDTH * 1.5;
+
+// We use this as a seed to generate the random values for the path
+// We use this as a seed to generate the random values for the path
+export const SEED_PHRASE =
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' +
+ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' +
+ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' +
+ 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ' +
+ 'Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. ' +
+ 'Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. ' +
+ 'Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra. ' +
+ 'Per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. Curabitur tortor. ' +
+ 'Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. ' +
+ 'Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. ' +
+ 'Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. ' +
+ 'Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. ' +
+ 'Nam nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. ' +
+ 'Nulla facilisi. Ut fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. ' +
+ 'Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. ' +
+ 'Sed lectus. Integer euismod lacus luctus magna. Quisque cursus, metus vitae pharetra auctor, sem massa mattis sem. ' +
+ 'At interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; ' +
+ 'Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. ' +
+ 'Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. ' +
+ 'Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper.';
diff --git a/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.model.ts b/src/common/components/mock-components/front-low-wireframes-components/text-scribbled-shape/text-scribbled.model.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/core/model/index.ts b/src/core/model/index.ts
index de8ffe0f..78191d2a 100644
--- a/src/core/model/index.ts
+++ b/src/core/model/index.ts
@@ -82,7 +82,8 @@ export type ShapeType =
| 'verticalLineLow'
| 'ellipseLow'
| 'rectangleLow'
- | 'circleLow';
+ | 'circleLow'
+ | 'textScribbled';
export const ShapeDisplayName: Record = {
multiple: 'multiple',
@@ -154,6 +155,7 @@ export const ShapeDisplayName: Record = {
ellipseLow: 'Ellipse',
rectangleLow: 'Rectangle Placeholder',
circleLow: 'Circle',
+ textScribbled: 'Text Scribbled',
};
export type EditType = 'input' | 'textarea' | 'imageupload';
diff --git a/src/pods/canvas/model/shape-size.mapper.ts b/src/pods/canvas/model/shape-size.mapper.ts
index 034c769e..1c3f0052 100644
--- a/src/pods/canvas/model/shape-size.mapper.ts
+++ b/src/pods/canvas/model/shape-size.mapper.ts
@@ -85,6 +85,7 @@ import {
getRectangleLowShapeRestrictions,
getEllipseLowShapeRestrictions,
getCircleLowShapeSizeRestrictions,
+ getTextScribbledShapeRestrictions,
} from '@/common/components/mock-components/front-low-wireframes-components';
const getMultipleNodeSizeRestrictions = (): ShapeSizeRestrictions => ({
@@ -167,6 +168,7 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = {
ellipseLow: getEllipseLowShapeRestrictions,
rectangleLow: getRectangleLowShapeRestrictions,
circleLow: getCircleLowShapeSizeRestrictions,
+ textScribbled: getTextScribbledShapeRestrictions,
};
export default shapeSizeMap;
diff --git a/src/pods/canvas/model/transformer.model.ts b/src/pods/canvas/model/transformer.model.ts
index a28695a4..bf601b42 100644
--- a/src/pods/canvas/model/transformer.model.ts
+++ b/src/pods/canvas/model/transformer.model.ts
@@ -49,6 +49,7 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => {
case 'datepickerinput':
case 'horizontalLine':
case 'horizontalLineLow':
+ case 'textScribbled':
case 'listbox':
case 'checkbox':
case 'toggleswitch':
diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx
index 92b85522..1ca9a691 100644
--- a/src/pods/canvas/shape-renderer/index.tsx
+++ b/src/pods/canvas/shape-renderer/index.tsx
@@ -80,6 +80,7 @@ import {
renderVerticalLowLine,
renderEllipseLow,
renderRectangleLow,
+ renderTextScribbled,
} from './simple-low-wireframes-components';
export const renderShapeComponent = (
@@ -223,6 +224,8 @@ export const renderShapeComponent = (
return renderRectangleLow(shape, shapeRenderedProps);
case 'circleLow':
return renderCircleLow(shape, shapeRenderedProps);
+ case 'textScribbled':
+ return renderTextScribbled(shape, shapeRenderedProps);
default:
return renderNotFound(shape, shapeRenderedProps);
}
diff --git a/src/pods/canvas/shape-renderer/simple-low-wireframes-components/index.ts b/src/pods/canvas/shape-renderer/simple-low-wireframes-components/index.ts
index ca8ea9da..714172a3 100644
--- a/src/pods/canvas/shape-renderer/simple-low-wireframes-components/index.ts
+++ b/src/pods/canvas/shape-renderer/simple-low-wireframes-components/index.ts
@@ -4,3 +4,4 @@ export * from './low-horizontal-line.renderer';
export * from './low-vertical-line.renderer';
export * from './rectangle-low.renderer';
export * from './circle-low.renderer';
+export * from './text-scribbled.renderer';
diff --git a/src/pods/canvas/shape-renderer/simple-low-wireframes-components/text-scribbled.renderer.tsx b/src/pods/canvas/shape-renderer/simple-low-wireframes-components/text-scribbled.renderer.tsx
new file mode 100644
index 00000000..d8d40891
--- /dev/null
+++ b/src/pods/canvas/shape-renderer/simple-low-wireframes-components/text-scribbled.renderer.tsx
@@ -0,0 +1,32 @@
+import { ShapeRendererProps } from '../model';
+import { ShapeModel } from '@/core/model';
+import { TextScribbled } from '@/common/components/mock-components/front-low-wireframes-components';
+
+export const renderTextScribbled = (
+ shape: ShapeModel,
+ shapeRenderedProps: ShapeRendererProps
+) => {
+ const { handleSelected, shapeRefs, handleDragEnd, handleTransform } =
+ shapeRenderedProps;
+
+ return (
+
+ );
+};
diff --git a/src/pods/galleries/low-wireframe-gallery/low-wireframe-gallery-data/index.ts b/src/pods/galleries/low-wireframe-gallery/low-wireframe-gallery-data/index.ts
index 90bc575f..f309ac20 100644
--- a/src/pods/galleries/low-wireframe-gallery/low-wireframe-gallery-data/index.ts
+++ b/src/pods/galleries/low-wireframe-gallery/low-wireframe-gallery-data/index.ts
@@ -25,4 +25,8 @@ export const mockLowWireframeCollection: ItemInfo[] = [
thumbnailSrc: '/low-wireframes/circleLow.svg',
type: 'circleLow',
},
+ {
+ thumbnailSrc: '/low-wireframes/textScribbled.svg',
+ type: 'textScribbled',
+ },
];