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', + }, ];