diff --git a/docs/src/app/(shaders)/dot-grid/page.tsx b/docs/src/app/(shaders)/dot-grid/page.tsx index 23a0f01f8..21ce70841 100644 --- a/docs/src/app/(shaders)/dot-grid/page.tsx +++ b/docs/src/app/(shaders)/dot-grid/page.tsx @@ -20,18 +20,24 @@ const DotGridWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFill: { value: toHsla(defaults.colorFill), order: 101 }, colorStroke: { value: toHsla(defaults.colorStroke), order: 102 }, - size: { value: defaults.size, min: 1, max: 100, order: 200 }, + shape: { + value: defaults.shape, + options: Object.keys(DotGridShapes) as DotGridShape[], + order: 103, + }, + size: { value: defaults.size, min: 0, max: 1, order: 200 }, gapX: { value: defaults.gapX, min: 2, max: 500, order: 201 }, gapY: { value: defaults.gapY, min: 2, max: 500, order: 202 }, strokeWidth: { value: defaults.strokeWidth, min: 0, max: 50, order: 203 }, sizeRange: { value: defaults.sizeRange, min: 0, max: 1, order: 204 }, opacityRange: { value: defaults.opacityRange, min: 0, max: 1, order: 205 }, - shape: { - value: defaults.shape, - options: Object.keys(DotGridShapes) as DotGridShape[], - order: 199, - }, - rotation: { value: defaults.rotation, min: 0, max: 360, order: 303 }, + angle: { value: defaults.angle, min: 0, max: 180, order: 300 }, + angleRange: { value: defaults.angleRange, min: 0, max: 1, order: 301 }, + rowShift: { value: defaults.shiftX, min: 0, max: 1, order: 302 }, + rowShiftRange: { value: defaults.rowShiftRange, min: 0, max: 1, order: 303 }, + shiftX: { value: defaults.shiftX, min: -100, max: 100, order: 320 }, + shiftY: { value: defaults.shiftY, min: -100, max: 100, order: 321 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 330 }, }; }); diff --git a/docs/src/shader-defs/dot-grid-def.ts b/docs/src/shader-defs/dot-grid-def.ts index 1cb7ec220..8c3762fe7 100644 --- a/docs/src/shader-defs/dot-grid-def.ts +++ b/docs/src/shader-defs/dot-grid-def.ts @@ -34,15 +34,15 @@ export const dotGridDef: ShaderDef = { type: 'enum', defaultValue: defaultParams.shape, description: 'The shape type', - options: ['circle', 'diamond', 'square', 'triangle'], + options: ['circle', 'diamond', 'square', 'triangle', 'star', 'asterisk', 'line', 'rect', 'cross', 'plus'], }, { name: 'size', type: 'number', - min: 1, - max: 100, + min: 0, + max: 1, defaultValue: defaultParams.size, - description: 'Base size of each shape, pixels', + description: 'Base size of each shape (0-1, relative to smaller cell side)', }, { name: 'gapX', @@ -84,6 +84,54 @@ export const dotGridDef: ShaderDef = { defaultValue: defaultParams.opacityRange, description: 'Random variation in shape opacity (0 = all shapes opaque, higher = semi-transparent dots)', }, + { + name: 'angle', + type: 'number', + min: 0, + max: 360, + defaultValue: defaultParams.angle, + description: 'Rotation of shape within each cell, degrees', + }, + { + name: 'angleRange', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.angleRange, + description: 'Random variation in cell angle (0 = uniform, higher = more random rotation)', + }, + { + name: 'shiftX', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.shiftX, + description: 'Horizontal offset of the pattern (0-1, relative to gapX)', + }, + { + name: 'shiftY', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.shiftY, + description: 'Vertical offset of the pattern (0-1, relative to gapY)', + }, + { + name: 'rowShift', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.rowShift, + description: 'Horizontal shift for every 2nd row (brick pattern) (0-1, relative to gapX)', + }, + { + name: 'rowShiftRange', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.rowShiftRange, + description: 'Randomize row shift (0 = only 2nd row shifts, 1 = all rows shift randomly)', + }, ...staticCommonParams, ], }; diff --git a/packages/shaders-react/src/shaders/dot-grid.tsx b/packages/shaders-react/src/shaders/dot-grid.tsx index 67c378d5b..6a4996a49 100644 --- a/packages/shaders-react/src/shaders/dot-grid.tsx +++ b/packages/shaders-react/src/shaders/dot-grid.tsx @@ -23,13 +23,19 @@ export const defaultPreset: DotGridPreset = { colorBack: '#000000', colorFill: '#ffffff', colorStroke: '#ffaa00', - size: 2, + size: 0.05, gapX: 32, gapY: 32, strokeWidth: 0, sizeRange: 0, opacityRange: 0, shape: 'circle', + angle: 0, + angleRange: 0, + shiftX: 0, + shiftY: 0, + rowShift: 0, + rowShiftRange: 0, }, }; @@ -37,16 +43,22 @@ const trianglesPreset: DotGridPreset = { name: 'Triangles', params: { ...defaultPatternSizing, - colorBack: '#ffffff', + colorBack: '#000000', colorFill: '#ffffff', - colorStroke: '#808080', - size: 5, + colorStroke: '#ffffff', + size: 1, gapX: 32, gapY: 32, - strokeWidth: 1, + strokeWidth: 0, sizeRange: 0, opacityRange: 0, shape: 'triangle', + angle: 0, + angleRange: 1, + shiftX: 0, + shiftY: 0, + rowShift: 0, + rowShiftRange: 0, }, }; @@ -57,13 +69,19 @@ const treeLinePreset: DotGridPreset = { colorBack: '#f4fce7', colorFill: '#052e19', colorStroke: '#000000', - size: 8, + size: 0.8, gapX: 20, gapY: 90, strokeWidth: 0, sizeRange: 1, opacityRange: 0.6, shape: 'circle', + angle: 0, + angleRange: 0, + shiftX: 0, + shiftY: 0, + rowShift: 0, + rowShiftRange: 0, }, }; @@ -71,20 +89,153 @@ const wallpaperPreset: DotGridPreset = { name: 'Wallpaper', params: { ...defaultPatternSizing, - colorBack: '#204030', + colorBack: '#4b2033', colorFill: '#000000', colorStroke: '#bd955b', - size: 9, + size: 0.55, gapX: 32, gapY: 32, strokeWidth: 1, sizeRange: 0, opacityRange: 0, shape: 'diamond', + angle: 0, + angleRange: 0, + shiftX: 0, + shiftY: 0, + rowShift: 0.5, + rowShiftRange: 0, + }, +}; + +const snowPreset: DotGridPreset = { + name: 'Snow', + params: { + ...defaultPatternSizing, + colorBack: '#1b1e31', + colorFill: '#ffffff', + colorStroke: '#000000', + size: 0.6, + gapX: 45, + gapY: 35, + strokeWidth: 0, + sizeRange: 0, + opacityRange: 1, + shape: 'asterisk', + angle: 0, + angleRange: 1, + shiftX: 0, + shiftY: 0, + rowShift: 0, + rowShiftRange: 0, + }, +}; + +const bricksPreset: DotGridPreset = { + name: 'Bricks', + params: { + ...defaultPatternSizing, + colorBack: '#ff9e9e', + colorFill: '#00ffb3', + colorStroke: '#000000', + size: 0.2, + gapX: 70, + gapY: 70, + strokeWidth: 0, + sizeRange: 0, + opacityRange: 0.05, + shape: 'rect', + angle: 0, + angleRange: 1, + shiftX: 0, + shiftY: 0, + rowShift: 0.5, + rowShiftRange: 0.25, + }, +}; + +const starsPreset: DotGridPreset = { + name: 'Stars', + params: { + ...defaultPatternSizing, + colorBack: '#000000', + colorFill: '#4294d7', + colorStroke: '#ffee00', + size: 0.75, + gapX: 25, + gapY: 25, + strokeWidth: 1, + sizeRange: 0.65, + opacityRange: 1, + shape: 'star', + angle: 0, + angleRange: 1, + shiftX: 0, + shiftY: 0, + rowShift: 0.5, + rowShiftRange: 0, + }, +}; + +const cellsPreset: DotGridPreset = { + name: 'Cells', + params: { + ...defaultPatternSizing, + colorBack: '#ffffff00', + colorFill: '#94d2ff', + colorStroke: '#203979', + size: 0.15, + gapX: 80, + gapY: 55, + strokeWidth: 4, + sizeRange: 0, + opacityRange: 0.05, + shape: 'cross', + angle: 0, + angleRange: 0, + shiftX: 0, + shiftY: 0, + rowShift: 0, + rowShiftRange: 0, + rotation: 148, + }, +}; + +const crossPreset: DotGridPreset = { + name: 'Cross', + params: { + ...defaultPatternSizing, + colorBack: '#ffffff00', + colorFill: '#000000', + colorStroke: '#000000', + size: 0.01, + gapX: 100, + gapY: 45, + strokeWidth: 0, + sizeRange: 0, + opacityRange: 0, + shape: 'cross', + angle: 90, + angleRange: 1, + shiftX: 0, + shiftY: 0, + rowShift: 0.65, + rowShiftRange: 0, + rotation: 260, }, }; -export const dotGridPresets: DotGridPreset[] = [defaultPreset, trianglesPreset, treeLinePreset, wallpaperPreset]; +export const dotGridPresets: DotGridPreset[] = [ + defaultPreset, + trianglesPreset, + treeLinePreset, + wallpaperPreset, + snowPreset, + bricksPreset, + starsPreset, + cellsPreset, + crossPreset, +]; export const DotGrid: React.FC = memo(function DotGridImpl({ // Own props @@ -98,6 +249,12 @@ export const DotGrid: React.FC = memo(function DotGridImpl({ sizeRange = defaultPreset.params.sizeRange, opacityRange = defaultPreset.params.opacityRange, shape = defaultPreset.params.shape, + angle = defaultPreset.params.angle, + angleRange = defaultPreset.params.angleRange, + shiftX = defaultPreset.params.shiftX, + shiftY = defaultPreset.params.shiftY, + rowShift = defaultPreset.params.rowShift, + rowShiftRange = defaultPreset.params.rowShiftRange, // Sizing props fit = defaultPreset.params.fit, @@ -126,6 +283,12 @@ export const DotGrid: React.FC = memo(function DotGridImpl({ u_sizeRange: sizeRange, u_opacityRange: opacityRange, u_shape: DotGridShapes[shape], + u_angle: angle, + u_angleRange: angleRange, + u_shiftX: shiftX, + u_shiftY: shiftY, + u_rowShift: rowShift, + u_rowShiftRange: rowShiftRange, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/dot-grid.ts b/packages/shaders/src/shaders/dot-grid.ts index 8a53183e7..408b1ce0d 100644 --- a/packages/shaders/src/shaders/dot-grid.ts +++ b/packages/shaders/src/shaders/dot-grid.ts @@ -1,20 +1,26 @@ import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, simplexNoise } from '../shader-utils.js'; +import { declarePI, rotation2, simplexNoise } from '../shader-utils.js'; /** - * Static grid pattern made of circles, diamonds, squares or triangles. + * Static grid pattern made of circles, diamonds, squares, triangles, stars, asterisks, lines, rects, crosses or plus signs. * * Fragment shader uniforms: * - u_colorBack (vec4): Background color in RGBA * - u_colorFill (vec4): Shape fill color in RGBA * - u_colorStroke (vec4): Shape stroke color in RGBA - * - u_dotSize (float): Base size of each shape in pixels (1 to 100) + * - u_dotSize (float): Base size of each shape (0 to 1, relative to smaller cell side) * - u_gapX (float): Pattern horizontal spacing in pixels (2 to 500) * - u_gapY (float): Pattern vertical spacing in pixels (2 to 500) * - u_strokeWidth (float): Outline stroke width in pixels (0 to 50) * - u_sizeRange (float): Random variation in shape size, 0 = uniform, higher = random up to base size (0 to 1) * - u_opacityRange (float): Random variation in shape opacity, 0 = opaque, higher = semi-transparent (0 to 1) - * - u_shape (float): Shape type (0 = circle, 1 = diamond, 2 = square, 3 = triangle) + * - u_shape (float): Shape type (0 = circle, 1 = diamond, 2 = square, 3 = triangle, 4 = star, 5 = asterisk, 6 = line, 7 = rect, 8 = cross, 9 = plus) + * - u_angle (float): Rotation of shape within each cell in degrees (0 to 360) + * - u_angleRange (float): Random variation in cell angle, 0 = uniform, higher = more random rotation (0 to 1) + * - u_shiftX (float): Horizontal offset of the pattern (-1 to 1, relative to gapX) + * - u_shiftY (float): Vertical offset of the pattern (-1 to 1, relative to gapY) + * - u_rowShift (float): Horizontal shift for every 2nd row, brick pattern (0 to 1, relative to gapX) + * - u_rowShiftRange (float): Randomize row shift, 0 = only 2nd row shifts, 1 = all rows shift randomly (0 to 1) * * Vertex shader outputs (used in fragment shader): * - v_patternUV (vec2): UV coordinates in pixels (scaled by 0.01 for precision), with scale, rotation and offset applied @@ -48,13 +54,20 @@ uniform float u_strokeWidth; uniform float u_sizeRange; uniform float u_opacityRange; uniform float u_shape; +uniform float u_angle; +uniform float u_angleRange; +uniform float u_shiftX; +uniform float u_shiftY; +uniform float u_rowShift; +uniform float u_rowShiftRange; in vec2 v_patternUV; out vec4 fragColor; -${ declarePI } -${ simplexNoise } +${declarePI} +${rotation2} +${simplexNoise} float polygon(vec2 p, float N, float rot) { float a = atan(p.x, p.y) + rot; @@ -63,54 +76,171 @@ float polygon(vec2 p, float N, float rot) { return cos(floor(.5 + a / r) * r - a) * length(p); } +float sdStar5(vec2 p, float r, float rf) { + const vec2 k1 = vec2(0.809016994375, -0.587785252292); + const vec2 k2 = vec2(-k1.x, k1.y); + p.x = abs(p.x); + p -= 2. * max(dot(k1, p), 0.) * k1; + p -= 2. * max(dot(k2, p), 0.) * k2; + p.x = abs(p.x); + p.y -= r; + vec2 ba = rf * vec2(-k1.y, k1.x) - vec2(0, 1); + float h = clamp(dot(p, ba) / dot(ba, ba), 0., r); + return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y); +} + +float asterisk6(vec2 p, float size, float armCoeff) { + float line1 = abs(p.y); + vec2 p60 = rotate(p, -PI / 3.); + float line2 = abs(p60.y); + vec2 p120 = rotate(p, -2. * PI / 3.); + float line3 = abs(p120.y); + float armDist = min(min(line1, line2), line3); + return max(armDist / armCoeff, length(p) / size) * size; +} + +float hash(float n) { + return fract(sin(n) * 43758.5453123); +} + void main() { // x100 is a default multiplier between vertex and fragmant shaders // we use it to avoid UV presision issues - vec2 shape_uv = 100. * v_patternUV; - vec2 gap = max(abs(vec2(u_gapX, u_gapY)), vec2(1e-6)); - vec2 grid = fract(shape_uv / gap) + 1e-4; - vec2 grid_idx = floor(shape_uv / gap); - float sizeRandomizer = .5 + .8 * snoise(2. * vec2(grid_idx.x * 100., grid_idx.y)); - float opacity_randomizer = .5 + .7 * snoise(2. * vec2(grid_idx.y, grid_idx.x)); + + // Shifts are relative to gaps (0-1 range means 0-100% of gap) + vec2 shapeUV = 100. * v_patternUV + vec2(-u_shiftX * gap.x, u_shiftY * gap.y); + if (u_shape > 3.) { + shapeUV -= 1e-4; + } + + // Row shift: every 2nd row shifts by rowShift, randomized by rowShiftRange + // rowShift is relative to gapX (0-1 range) + float rowIndex = floor(shapeUV.y / gap.y); + float rowShiftRandomizer = hash(rowIndex * 73.129); + float rowShiftAmount = u_rowShift * gap.x * mix(mod(rowIndex, 2.), 3. * rowShiftRandomizer, u_rowShiftRange); + shapeUV.x += rowShiftAmount; + + vec2 gridF = fract(shapeUV / gap) + 1e-4; + vec2 gridI = floor(shapeUV / gap); + float sizeRandomizer = .5 + .8 * snoise(2. * vec2(gridI.x * 100., gridI.y)); + float opacityRandomizer = .5 + .7 * snoise(2. * vec2(gridI.y, gridI.x)); + float angleRandomizer = hash(gridI.x * 37.197 + gridI.y * 91.853) * 2. - 1.; vec2 center = vec2(0.5) - 1e-3; - vec2 p = (grid - center) * vec2(u_gapX, u_gapY); + vec2 p = (gridF - center) * vec2(u_gapX, u_gapY); - float baseSize = u_dotSize * (1. - sizeRandomizer * u_sizeRange); + // Size is relative to smaller cell side (0-1 range means 0-100% of min gap) + float minGap = min(gap.x, gap.y); + float baseSize = u_dotSize * minGap * (1. - sizeRandomizer * u_sizeRange); float strokeWidth = u_strokeWidth * (1. - sizeRandomizer * u_sizeRange); + float cellAngleRad = u_angle * PI / 180. + angleRandomizer * u_angleRange * PI; + float dist; + float edgeW = fwidth(shapeUV.y); + + if (u_shape != 3.) { + p = rotate(p, cellAngleRad); + } + baseSize *= .5; if (u_shape < 0.5) { - // Circle + // Circle (0) dist = length(p); } else if (u_shape < 1.5) { - // Diamond - strokeWidth *= 1.5; - dist = polygon(1.5 * p, 4., .25 * PI); + // Diamond (1) + baseSize *= .7071; + dist = polygon(p, 4., .25 * PI); } else if (u_shape < 2.5) { - // Square - dist = polygon(1.03 * p, 4., 1e-3); + // Square (2) + baseSize *= .7071; + dist = polygon(p, 4., 0.); + } else if (u_shape < 3.5) { + // Triangle (3) + baseSize *= .5; + dist = polygon(p, 3., - .333333333333 * PI - cellAngleRad); + } else if (u_shape < 4.5) { + // Star (4) - compute outer star for non-rounded stroke + float innerSDF = sdStar5(p, baseSize, .5); + float scale = (baseSize + strokeWidth * 2.) / baseSize; + float outerSDF = sdStar5(p / scale, baseSize, .5) * scale; + float strokeInner = 1. - smoothstep(-edgeW, edgeW, innerSDF); + float starOuter = 1. - smoothstep(-edgeW, edgeW, outerSDF); + float stroke = starOuter - strokeInner; + + float shapeOpacity = max(0., 1. - opacityRandomizer * u_opacityRange); + stroke *= shapeOpacity; + strokeInner *= shapeOpacity; + stroke *= u_colorStroke.a; + strokeInner *= u_colorFill.a; + + vec3 color = vec3(0.); + color += stroke * u_colorStroke.rgb; + color += strokeInner * u_colorFill.rgb; + color += (1. - strokeInner - stroke) * u_colorBack.rgb * u_colorBack.a; + + float opacity = stroke + strokeInner + (1. - stroke - strokeInner) * u_colorBack.a; + fragColor = vec4(color, opacity); + return; + } else if (u_shape < 5.5) { + // Asterisk (5) - expand both size and arm width for consistent stroke + float armCoeff = baseSize * .15; + float innerSDF = asterisk6(p, baseSize, armCoeff) - baseSize; + float outerSDF = asterisk6(p, baseSize + strokeWidth, armCoeff + strokeWidth) - (baseSize + strokeWidth); + float strokeInner = 1. - smoothstep(-edgeW, edgeW, innerSDF); + float astOuter = 1. - smoothstep(-edgeW, edgeW, outerSDF); + float stroke = astOuter - strokeInner; + + float shapeOpacity = max(0., 1. - opacityRandomizer * u_opacityRange); + stroke *= shapeOpacity; + strokeInner *= shapeOpacity; + stroke *= u_colorStroke.a; + strokeInner *= u_colorFill.a; + + vec3 color = vec3(0.); + color += stroke * u_colorStroke.rgb; + color += strokeInner * u_colorFill.rgb; + color += (1. - strokeInner - stroke) * u_colorBack.rgb * u_colorBack.a; + + float opacity = stroke + strokeInner + (1. - stroke - strokeInner) * u_colorBack.a; + fragColor = vec4(color, opacity); + return; + } else if (u_shape < 6.5) { + // Line (6) + dist = abs(p.y); + } else if (u_shape < 7.5) { + // Rect (7) + float maxH = min(u_gapX / 6., u_gapY / 6.); + baseSize = min(baseSize, maxH); + float margin = baseSize; + float maxWfromX = (u_gapX * .5 - margin - baseSize); + float maxWfromY = (u_gapY * .5 - margin - baseSize); + float halfWidth = max(baseSize, min(maxWfromX, maxWfromY)); + dist = max(abs(p.y), max(0., abs(p.x) - halfWidth)); + } else if (u_shape < 8.5) { + // Cross (8) + dist = min(abs(p.x), abs(p.y)); } else { - // Triangle - strokeWidth *= 1.5; - p = p * 2. - 1.; - p *= .9; - p.y = 1. - p.y; - p.y -= .75 * baseSize; - dist = polygon(p, 3., 1e-3); + // Plus (9) + float maxH = min(u_gapX / 6., u_gapY / 6.); + baseSize = min(baseSize, maxH); + float margin = baseSize; + float maxLfromX = (u_gapX * .5 - margin - baseSize); + float maxLfromY = (u_gapY * .5 - margin - baseSize); + float armLength = max(baseSize, min(maxLfromX, maxLfromY)); + float crossDist = min(abs(p.x), abs(p.y)); + float armCrop = max(0., max(abs(p.x), abs(p.y)) - armLength); + dist = max(crossDist, armCrop); } - float edgeWidth = fwidth(dist); - float shapeOuter = 1. - smoothstep(baseSize - edgeWidth, baseSize + edgeWidth, dist - strokeWidth); - float shapeInner = 1. - smoothstep(baseSize - edgeWidth, baseSize + edgeWidth, dist); + float shapeOuter = 1. - smoothstep(baseSize - edgeW, baseSize + edgeW, dist - strokeWidth); + float shapeInner = 1. - smoothstep(baseSize - edgeW, baseSize + edgeW, dist); float stroke = shapeOuter - shapeInner; - float dotOpacity = max(0., 1. - opacity_randomizer * u_opacityRange); - stroke *= dotOpacity; - shapeInner *= dotOpacity; - + float shapeOpacity = max(0., 1. - opacityRandomizer * u_opacityRange); + stroke *= shapeOpacity; + shapeInner *= shapeOpacity; stroke *= u_colorStroke.a; shapeInner *= u_colorFill.a; @@ -119,9 +249,7 @@ void main() { color += shapeInner * u_colorFill.rgb; color += (1. - shapeInner - stroke) * u_colorBack.rgb * u_colorBack.a; - float opacity = 0.; - opacity += stroke; - opacity += shapeInner; + float opacity = stroke + shapeInner; opacity += (1. - opacity) * u_colorBack.a; fragColor = vec4(color, opacity); @@ -139,6 +267,12 @@ export interface DotGridUniforms extends ShaderSizingUniforms { u_sizeRange: number; u_opacityRange: number; u_shape: (typeof DotGridShapes)[DotGridShape]; + u_angle: number; + u_angleRange: number; + u_shiftX: number; + u_shiftY: number; + u_rowShift: number; + u_rowShiftRange: number; } export interface DotGridParams extends ShaderSizingParams { @@ -152,6 +286,12 @@ export interface DotGridParams extends ShaderSizingParams { sizeRange?: number; opacityRange?: number; shape?: DotGridShape; + angle?: number; + angleRange?: number; + shiftX?: number; + shiftY?: number; + rowShift?: number; + rowShiftRange?: number; } export const DotGridShapes = { @@ -159,6 +299,12 @@ export const DotGridShapes = { diamond: 1, square: 2, triangle: 3, + star: 4, + asterisk: 5, + line: 6, + rect: 7, + cross: 8, + plus: 9, } as const; export type DotGridShape = keyof typeof DotGridShapes;