diff --git a/packages/core/examples/main.ts b/packages/core/examples/main.ts index 707ade6..cd15c00 100644 --- a/packages/core/examples/main.ts +++ b/packages/core/examples/main.ts @@ -1,6 +1,7 @@ import { Canvas, Rect, + RoughCircle, RoughRect, Path, fromSVGElement, @@ -25,44 +26,20 @@ const canvas = await new Canvas({ // shaderCompilerPath: '/glsl_wgsl_compiler_bg.wasm', }).initialized; -// fetch( -// '/Ghostscript_Tiger.svg', -// // '/photo-camera.svg', -// ).then(async (res) => { -// const svg = await res.text(); - -// const $container = document.createElement('div'); -// $container.innerHTML = svg; - -// const $svg = $container.children[0]; - -// for (const child of $svg.children) { -// const group = await deserializeNode(fromSVGElement(child as SVGElement)); -// canvas.appendChild(group); -// } -// }); - -// const path = new Path({ -// d, -// fill: 'none', -// strokeWidth: 2, -// stroke: 'red', -// }); -// canvas.appendChild(path); -// }); - -// const rect = new Rect({ -// x: 0, -// y: 0, -// width: 100, -// height: 100, -// fill: 'black', -// strokeWidth: 2, -// stroke: 'red', -// }); -// canvas.appendChild(rect); +const circle = new RoughCircle({ + cx: 0, + cy: 0, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + seed: 1, + roughness: 1, + fillStyle: 'dots', +}); +canvas.appendChild(circle); -const ring = new RoughRect({ +const rect = new RoughRect({ x: 0, y: 0, width: 100, @@ -74,15 +51,15 @@ const ring = new RoughRect({ roughness: 1, fillStyle: 'dots', }); -ring.position.x = 200; -ring.position.y = 200; -canvas.appendChild(ring); +rect.position.x = 200; +rect.position.y = 200; +canvas.appendChild(rect); -ring.addEventListener('pointerenter', () => { - ring.fill = 'blue'; +rect.addEventListener('pointerenter', () => { + rect.fill = 'blue'; }); -ring.addEventListener('pointerleave', () => { - ring.fill = 'black'; +rect.addEventListener('pointerleave', () => { + rect.fill = 'black'; }); // setTimeout(() => { diff --git a/packages/core/src/drawcalls/BatchManager.ts b/packages/core/src/drawcalls/BatchManager.ts index 5a29b62..1a251ca 100644 --- a/packages/core/src/drawcalls/BatchManager.ts +++ b/packages/core/src/drawcalls/BatchManager.ts @@ -6,6 +6,7 @@ import { Path, Polyline, Rect, + RoughCircle, RoughRect, type Shape, } from '../shapes'; @@ -28,6 +29,12 @@ SHAPE_DRAWCALL_CTORS.set(Rect, [ShadowRect, SDF, SmoothPolyline]); SHAPE_DRAWCALL_CTORS.set(Polyline, [SmoothPolyline]); // SHAPE_DRAWCALL_CTORS.set(Path, [SDFPath]); SHAPE_DRAWCALL_CTORS.set(Path, [Mesh, SmoothPolyline]); +// @ts-expect-error Property 'getGeometryBounds' is missing in type 'RoughCircle' +SHAPE_DRAWCALL_CTORS.set(RoughCircle, [ + Mesh, // fillStyle === 'solid' + SmoothPolyline, // fill + SmoothPolyline, // stroke +]); // @ts-expect-error Property 'getGeometryBounds' is missing in type 'RoughRect' SHAPE_DRAWCALL_CTORS.set(RoughRect, [ ShadowRect, diff --git a/packages/core/src/drawcalls/Mesh.ts b/packages/core/src/drawcalls/Mesh.ts index 6867bd6..65dd53f 100644 --- a/packages/core/src/drawcalls/Mesh.ts +++ b/packages/core/src/drawcalls/Mesh.ts @@ -18,7 +18,7 @@ import { Texture, StencilOp, } from '@antv/g-device-api'; -import { Path, RoughRect, TesselationMethod } from '../shapes'; +import { Path, RoughCircle, RoughRect, TesselationMethod } from '../shapes'; import { Drawcall, ZINDEX_FACTOR } from './Drawcall'; import { vert, frag, Location } from '../shaders/mesh'; import { isString, paddingMat3, triangulate } from '../utils'; @@ -91,7 +91,10 @@ export class Mesh extends Drawcall { if (instance instanceof Path) { rawPoints = instance.points; tessellationMethod = instance.tessellationMethod; - } else if (instance instanceof RoughRect) { + } else if ( + instance instanceof RoughCircle || + instance instanceof RoughRect + ) { rawPoints = instance.fillPathPoints; tessellationMethod = TesselationMethod.EARCUT; } diff --git a/packages/core/src/drawcalls/SmoothPolyline.ts b/packages/core/src/drawcalls/SmoothPolyline.ts index b62845d..076ce33 100644 --- a/packages/core/src/drawcalls/SmoothPolyline.ts +++ b/packages/core/src/drawcalls/SmoothPolyline.ts @@ -23,6 +23,7 @@ import { Path, Polyline, Rect, + RoughCircle, RoughRect, Shape, hasValidStroke, @@ -45,6 +46,7 @@ export class SmoothPolyline extends Drawcall { static check(shape: Shape) { return ( shape instanceof Polyline || + shape instanceof RoughCircle || shape instanceof RoughRect || ((shape instanceof Rect || shape instanceof Circle || @@ -80,6 +82,7 @@ export class SmoothPolyline extends Drawcall { if ( instance instanceof Polyline || instance instanceof Path || + instance instanceof RoughCircle || instance instanceof RoughRect ) { return this.pointsBuffer.length / strideFloats - 3; @@ -101,7 +104,8 @@ export class SmoothPolyline extends Drawcall { this.shapes.forEach((shape: Polyline) => { const { pointsBuffer: pBuffer, travelBuffer: tBuffer } = updateBuffer( shape, - this.index === 3, + (shape instanceof RoughCircle && this.index === 2) || + (shape instanceof RoughRect && this.index === 3), ); pointsBuffer.push(...pBuffer); @@ -475,7 +479,10 @@ export class SmoothPolyline extends Drawcall { 0, ]; - if (instance instanceof RoughRect && this.index === 2) { + if ( + (instance instanceof RoughCircle && this.index === 1) || + (instance instanceof RoughRect && this.index === 2) + ) { u_StrokeColor = [fr / 255, fg / 255, fb / 255, fo]; u_Opacity[2] = fillOpacity; } @@ -534,7 +541,7 @@ export function updateBuffer(object: Shape, useRoughStroke = true) { let points: number[] = []; // const triangles: number[] = []; - if (object instanceof RoughRect) { + if (object instanceof RoughCircle || object instanceof RoughRect) { const { strokePoints, fillPoints } = object; points = (useRoughStroke ? strokePoints : fillPoints) .map((subPathPoints, i) => { diff --git a/packages/core/src/shapes/Circle.ts b/packages/core/src/shapes/Circle.ts index 4e971cd..baff83a 100644 --- a/packages/core/src/shapes/Circle.ts +++ b/packages/core/src/shapes/Circle.ts @@ -6,6 +6,7 @@ import { } from './Shape'; import { distanceBetweenPoints } from '../utils'; import { AABB } from './AABB'; +import { GConstructor } from './mixins'; export interface CircleAttributes extends ShapeAttributes { /** @@ -28,124 +29,130 @@ export interface CircleAttributes extends ShapeAttributes { r: number; } -export class Circle extends Shape implements CircleAttributes { - #cx: number; - #cy: number; - #r: number; - - static getGeometryBounds( - attributes: Partial>, - ) { - const { cx = 0, cy = 0, r = 0 } = attributes; - return new AABB(cx - r, cy - r, cx + r, cy + r); - } - - constructor(attributes: Partial = {}) { - super(attributes); - - const { cx, cy, r } = attributes; - - this.cx = cx ?? 0; - this.cy = cy ?? 0; - this.r = r ?? 0; - } - - get cx() { - return this.#cx; - } - - set cx(cx: number) { - if (this.#cx !== cx) { - this.#cx = cx; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-redeclare +export class Circle extends CircleWrapper(Shape) {} +export function CircleWrapper(Base: TBase) { + // @ts-expect-error - Mixin class + return class CircleWrapper extends Base implements CircleAttributes { + #cx: number; + #cy: number; + #r: number; + + static getGeometryBounds( + attributes: Partial>, + ) { + const { cx = 0, cy = 0, r = 0 } = attributes; + return new AABB(cx - r, cy - r, cx + r, cy + r); } - } - - get cy() { - return this.#cy; - } - - set cy(cy: number) { - if (this.#cy !== cy) { - this.#cy = cy; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; + + constructor(attributes: Partial = {}) { + super(attributes); + + const { cx, cy, r } = attributes; + + this.cx = cx ?? 0; + this.cy = cy ?? 0; + this.r = r ?? 0; } - } - - get r() { - return this.#r; - } - - set r(r: number) { - if (this.#r !== r) { - this.#r = r; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; + + get cx() { + return this.#cx; } - } - - containsPoint(x: number, y: number) { - const { - strokeWidth, - strokeAlignment, - cx, - cy, - r, - pointerEvents, - fill, - stroke, - } = this; - - const absDistance = distanceBetweenPoints(cx, cy, x, y); - const offset = strokeOffset(strokeAlignment, strokeWidth); - - const [hasFill, hasStroke] = isFillOrStrokeAffected( - pointerEvents, - fill, - stroke, - ); - if (hasFill && hasStroke) { - return absDistance <= r + offset; + + set cx(cx: number) { + if (this.#cx !== cx) { + this.#cx = cx; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } } - if (hasFill) { - return absDistance <= r; + + get cy() { + return this.#cy; } - if (hasStroke) { - return ( - absDistance >= r + offset - strokeWidth && absDistance <= r + offset - ); + + set cy(cy: number) { + if (this.#cy !== cy) { + this.#cy = cy; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } } - return false; - } - getGeometryBounds() { - if (this.geometryBoundsDirtyFlag) { - this.geometryBoundsDirtyFlag = false; - this.geometryBounds = Circle.getGeometryBounds(this); + get r() { + return this.#r; } - return this.geometryBounds; - } - getRenderBounds() { - if (this.renderBoundsDirtyFlag) { - const { strokeWidth, strokeAlignment, cx, cy, r } = this; + set r(r: number) { + if (this.#r !== r) { + this.#r = r; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } + } + + containsPoint(x: number, y: number) { + const { + strokeWidth, + strokeAlignment, + cx, + cy, + r, + pointerEvents, + fill, + stroke, + } = this; + + const absDistance = distanceBetweenPoints(cx, cy, x, y); const offset = strokeOffset(strokeAlignment, strokeWidth); - this.renderBoundsDirtyFlag = false; - this.renderBounds = new AABB( - cx - r - offset, - cy - r - offset, - cx + r + offset, - cy + r + offset, + + const [hasFill, hasStroke] = isFillOrStrokeAffected( + pointerEvents, + fill, + stroke, ); + if (hasFill && hasStroke) { + return absDistance <= r + offset; + } + if (hasFill) { + return absDistance <= r; + } + if (hasStroke) { + return ( + absDistance >= r + offset - strokeWidth && absDistance <= r + offset + ); + } + return false; + } + + getGeometryBounds() { + if (this.geometryBoundsDirtyFlag) { + this.geometryBoundsDirtyFlag = false; + this.geometryBounds = Circle.getGeometryBounds(this); + } + return this.geometryBounds; + } + + getRenderBounds() { + if (this.renderBoundsDirtyFlag) { + const { strokeWidth, strokeAlignment, cx, cy, r } = this; + const offset = strokeOffset(strokeAlignment, strokeWidth); + this.renderBoundsDirtyFlag = false; + this.renderBounds = new AABB( + cx - r - offset, + cy - r - offset, + cx + r + offset, + cy + r + offset, + ); + } + return this.renderBounds; } - return this.renderBounds; - } + }; } diff --git a/packages/core/src/shapes/Rect.ts b/packages/core/src/shapes/Rect.ts index d6095f1..95b26f6 100644 --- a/packages/core/src/shapes/Rect.ts +++ b/packages/core/src/shapes/Rect.ts @@ -85,6 +85,8 @@ export function RectWrapper(Base: TBase) { #dropShadowOffsetY: number; #dropShadowBlurRadius: number; + onGeometryChanged?: () => void; + static getGeometryBounds( attributes: Partial>, ) { @@ -128,6 +130,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -141,6 +144,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -154,6 +158,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -167,6 +172,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } diff --git a/packages/core/src/shapes/RoughCircle.ts b/packages/core/src/shapes/RoughCircle.ts new file mode 100644 index 0000000..d490b33 --- /dev/null +++ b/packages/core/src/shapes/RoughCircle.ts @@ -0,0 +1,22 @@ +import { CircleWrapper, CircleAttributes } from './Circle'; +import { generator } from '../utils'; +import { IRough, Rough } from './mixins/Rough'; +import { Shape } from './Shape'; + +export interface RoughCircleAttributes extends CircleAttributes, IRough {} + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-redeclare +export class RoughCircle extends Rough(CircleWrapper(Shape)) { + constructor(attributes: Partial = {}) { + super(attributes); + } + + // TODO: cx / cy / r also regenerates the drawable + + generateDrawable() { + const { cx, cy, r } = this; + + return generator.circle(cx, cy, r * 2, this.roughOptions); + } +} diff --git a/packages/core/src/shapes/RoughRect.ts b/packages/core/src/shapes/RoughRect.ts index 81e81c4..15e3b01 100644 --- a/packages/core/src/shapes/RoughRect.ts +++ b/packages/core/src/shapes/RoughRect.ts @@ -1,5 +1,5 @@ import { RectWrapper, RectAttributes } from './Rect'; -import { filterUndefined, generator } from '../utils'; +import { generator } from '../utils'; import { IRough, Rough } from './mixins/Rough'; import { Shape } from './Shape'; @@ -10,39 +10,17 @@ export interface RoughRectAttributes extends RectAttributes, IRough {} export class RoughRect extends Rough(RectWrapper(Shape)) { constructor(attributes: Partial = {}) { super(attributes); + + // x / y / width / height also regenerates the drawable + this.onGeometryChanged = () => { + this.geometryDirtyFlag = true; + this.generate(); + }; } generateDrawable() { - const { - x, - y, - width, - height, - fill, - stroke, - strokeWidth, - seed, - bowing, - roughness, - fillStyle, - fillWeight, - } = this; + const { x, y, width, height } = this; - return generator.rectangle( - x, - y, - width, - height, - filterUndefined({ - fill: fill as string, - stroke, - strokeWidth, - seed, - bowing, - roughness, - fillStyle, - fillWeight, - }), - ); + return generator.rectangle(x, y, width, height, this.roughOptions); } } diff --git a/packages/core/src/shapes/index.ts b/packages/core/src/shapes/index.ts index 30e3beb..5a8151c 100644 --- a/packages/core/src/shapes/index.ts +++ b/packages/core/src/shapes/index.ts @@ -7,4 +7,5 @@ export * from './Polyline'; export * from './Path'; export * from './Grid'; export * from './Group'; +export * from './RoughCircle'; export * from './RoughRect'; diff --git a/packages/core/src/shapes/mixins/Rough.ts b/packages/core/src/shapes/mixins/Rough.ts index c403cc1..a0ba1cf 100644 --- a/packages/core/src/shapes/mixins/Rough.ts +++ b/packages/core/src/shapes/mixins/Rough.ts @@ -1,6 +1,6 @@ import { Drawable, Options } from 'roughjs/bin/core'; import { GConstructor } from '.'; -import { parsePath } from '../../utils'; +import { filterUndefined, parsePath } from '../../utils'; export interface IRough extends Omit { @@ -139,6 +139,29 @@ export function Rough(Base: TBase) { } } + get roughOptions(): Options { + const { + fill, + stroke, + strokeWidth, + seed, + bowing, + roughness, + fillStyle, + fillWeight, + } = this; + return filterUndefined({ + fill: fill as string, + stroke, + strokeWidth, + seed, + bowing, + roughness, + fillStyle, + fillWeight, + }); + } + /** * generate rough shape */ diff --git a/packages/lesson_013/examples/main.ts b/packages/lesson_013/examples/main.ts index 7859aa8..5336b8e 100644 --- a/packages/lesson_013/examples/main.ts +++ b/packages/lesson_013/examples/main.ts @@ -1,4 +1,10 @@ -import { Canvas, RoughRect, fromSVGElement, deserializeNode } from '../src'; +import { + Canvas, + RoughRect, + RoughCircle, + fromSVGElement, + deserializeNode, +} from '../src'; const $canvas = document.getElementById('canvas') as HTMLCanvasElement; const resize = (width: number, height: number) => { @@ -18,37 +24,48 @@ const canvas = await new Canvas({ // shaderCompilerPath: '/glsl_wgsl_compiler_bg.wasm', }).initialized; -fetch( - '/Ghostscript_Tiger.svg', - // '/photo-camera.svg', -).then(async (res) => { - const svg = await res.text(); +// fetch( +// '/Ghostscript_Tiger.svg', +// // '/photo-camera.svg', +// ).then(async (res) => { +// const svg = await res.text(); - const $container = document.createElement('div'); - $container.innerHTML = svg; +// const $container = document.createElement('div'); +// $container.innerHTML = svg; - const $svg = $container.children[0]; +// const $svg = $container.children[0]; - for (const child of $svg.children) { - const group = await deserializeNode(fromSVGElement(child as SVGElement)); - canvas.appendChild(group); - } -}); +// for (const child of $svg.children) { +// const group = await deserializeNode(fromSVGElement(child as SVGElement)); +// canvas.appendChild(group); +// } +// }); + +// const circle = new RoughCircle({ +// cx: 0, +// cy: 0, +// r: 50, +// fill: 'black', +// strokeWidth: 2, +// stroke: 'red', +// fillStyle: 'solid', +// }); +// canvas.appendChild(circle); const ring = new RoughRect({ x: 0, y: 0, - width: 100, - height: 100, + width: 200, + height: 200, fill: 'black', strokeWidth: 2, stroke: 'red', - seed: 1, - roughness: 1, - fillStyle: 'dots', + fillStyle: 'hachure', }); ring.position.x = 200; ring.position.y = 200; +ring.width = 100; +ring.height = 100; canvas.appendChild(ring); canvas.render(); diff --git a/packages/lesson_013/src/drawcalls/BatchManager.ts b/packages/lesson_013/src/drawcalls/BatchManager.ts index 5a29b62..1a251ca 100644 --- a/packages/lesson_013/src/drawcalls/BatchManager.ts +++ b/packages/lesson_013/src/drawcalls/BatchManager.ts @@ -6,6 +6,7 @@ import { Path, Polyline, Rect, + RoughCircle, RoughRect, type Shape, } from '../shapes'; @@ -28,6 +29,12 @@ SHAPE_DRAWCALL_CTORS.set(Rect, [ShadowRect, SDF, SmoothPolyline]); SHAPE_DRAWCALL_CTORS.set(Polyline, [SmoothPolyline]); // SHAPE_DRAWCALL_CTORS.set(Path, [SDFPath]); SHAPE_DRAWCALL_CTORS.set(Path, [Mesh, SmoothPolyline]); +// @ts-expect-error Property 'getGeometryBounds' is missing in type 'RoughCircle' +SHAPE_DRAWCALL_CTORS.set(RoughCircle, [ + Mesh, // fillStyle === 'solid' + SmoothPolyline, // fill + SmoothPolyline, // stroke +]); // @ts-expect-error Property 'getGeometryBounds' is missing in type 'RoughRect' SHAPE_DRAWCALL_CTORS.set(RoughRect, [ ShadowRect, diff --git a/packages/lesson_013/src/drawcalls/Mesh.ts b/packages/lesson_013/src/drawcalls/Mesh.ts index 6867bd6..65dd53f 100644 --- a/packages/lesson_013/src/drawcalls/Mesh.ts +++ b/packages/lesson_013/src/drawcalls/Mesh.ts @@ -18,7 +18,7 @@ import { Texture, StencilOp, } from '@antv/g-device-api'; -import { Path, RoughRect, TesselationMethod } from '../shapes'; +import { Path, RoughCircle, RoughRect, TesselationMethod } from '../shapes'; import { Drawcall, ZINDEX_FACTOR } from './Drawcall'; import { vert, frag, Location } from '../shaders/mesh'; import { isString, paddingMat3, triangulate } from '../utils'; @@ -91,7 +91,10 @@ export class Mesh extends Drawcall { if (instance instanceof Path) { rawPoints = instance.points; tessellationMethod = instance.tessellationMethod; - } else if (instance instanceof RoughRect) { + } else if ( + instance instanceof RoughCircle || + instance instanceof RoughRect + ) { rawPoints = instance.fillPathPoints; tessellationMethod = TesselationMethod.EARCUT; } diff --git a/packages/lesson_013/src/drawcalls/SmoothPolyline.ts b/packages/lesson_013/src/drawcalls/SmoothPolyline.ts index b62845d..076ce33 100644 --- a/packages/lesson_013/src/drawcalls/SmoothPolyline.ts +++ b/packages/lesson_013/src/drawcalls/SmoothPolyline.ts @@ -23,6 +23,7 @@ import { Path, Polyline, Rect, + RoughCircle, RoughRect, Shape, hasValidStroke, @@ -45,6 +46,7 @@ export class SmoothPolyline extends Drawcall { static check(shape: Shape) { return ( shape instanceof Polyline || + shape instanceof RoughCircle || shape instanceof RoughRect || ((shape instanceof Rect || shape instanceof Circle || @@ -80,6 +82,7 @@ export class SmoothPolyline extends Drawcall { if ( instance instanceof Polyline || instance instanceof Path || + instance instanceof RoughCircle || instance instanceof RoughRect ) { return this.pointsBuffer.length / strideFloats - 3; @@ -101,7 +104,8 @@ export class SmoothPolyline extends Drawcall { this.shapes.forEach((shape: Polyline) => { const { pointsBuffer: pBuffer, travelBuffer: tBuffer } = updateBuffer( shape, - this.index === 3, + (shape instanceof RoughCircle && this.index === 2) || + (shape instanceof RoughRect && this.index === 3), ); pointsBuffer.push(...pBuffer); @@ -475,7 +479,10 @@ export class SmoothPolyline extends Drawcall { 0, ]; - if (instance instanceof RoughRect && this.index === 2) { + if ( + (instance instanceof RoughCircle && this.index === 1) || + (instance instanceof RoughRect && this.index === 2) + ) { u_StrokeColor = [fr / 255, fg / 255, fb / 255, fo]; u_Opacity[2] = fillOpacity; } @@ -534,7 +541,7 @@ export function updateBuffer(object: Shape, useRoughStroke = true) { let points: number[] = []; // const triangles: number[] = []; - if (object instanceof RoughRect) { + if (object instanceof RoughCircle || object instanceof RoughRect) { const { strokePoints, fillPoints } = object; points = (useRoughStroke ? strokePoints : fillPoints) .map((subPathPoints, i) => { diff --git a/packages/lesson_013/src/shapes/Circle.ts b/packages/lesson_013/src/shapes/Circle.ts index 4e971cd..baff83a 100644 --- a/packages/lesson_013/src/shapes/Circle.ts +++ b/packages/lesson_013/src/shapes/Circle.ts @@ -6,6 +6,7 @@ import { } from './Shape'; import { distanceBetweenPoints } from '../utils'; import { AABB } from './AABB'; +import { GConstructor } from './mixins'; export interface CircleAttributes extends ShapeAttributes { /** @@ -28,124 +29,130 @@ export interface CircleAttributes extends ShapeAttributes { r: number; } -export class Circle extends Shape implements CircleAttributes { - #cx: number; - #cy: number; - #r: number; - - static getGeometryBounds( - attributes: Partial>, - ) { - const { cx = 0, cy = 0, r = 0 } = attributes; - return new AABB(cx - r, cy - r, cx + r, cy + r); - } - - constructor(attributes: Partial = {}) { - super(attributes); - - const { cx, cy, r } = attributes; - - this.cx = cx ?? 0; - this.cy = cy ?? 0; - this.r = r ?? 0; - } - - get cx() { - return this.#cx; - } - - set cx(cx: number) { - if (this.#cx !== cx) { - this.#cx = cx; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-redeclare +export class Circle extends CircleWrapper(Shape) {} +export function CircleWrapper(Base: TBase) { + // @ts-expect-error - Mixin class + return class CircleWrapper extends Base implements CircleAttributes { + #cx: number; + #cy: number; + #r: number; + + static getGeometryBounds( + attributes: Partial>, + ) { + const { cx = 0, cy = 0, r = 0 } = attributes; + return new AABB(cx - r, cy - r, cx + r, cy + r); } - } - - get cy() { - return this.#cy; - } - - set cy(cy: number) { - if (this.#cy !== cy) { - this.#cy = cy; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; + + constructor(attributes: Partial = {}) { + super(attributes); + + const { cx, cy, r } = attributes; + + this.cx = cx ?? 0; + this.cy = cy ?? 0; + this.r = r ?? 0; } - } - - get r() { - return this.#r; - } - - set r(r: number) { - if (this.#r !== r) { - this.#r = r; - this.renderDirtyFlag = true; - this.geometryBoundsDirtyFlag = true; - this.renderBoundsDirtyFlag = true; - this.boundsDirtyFlag = true; + + get cx() { + return this.#cx; } - } - - containsPoint(x: number, y: number) { - const { - strokeWidth, - strokeAlignment, - cx, - cy, - r, - pointerEvents, - fill, - stroke, - } = this; - - const absDistance = distanceBetweenPoints(cx, cy, x, y); - const offset = strokeOffset(strokeAlignment, strokeWidth); - - const [hasFill, hasStroke] = isFillOrStrokeAffected( - pointerEvents, - fill, - stroke, - ); - if (hasFill && hasStroke) { - return absDistance <= r + offset; + + set cx(cx: number) { + if (this.#cx !== cx) { + this.#cx = cx; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } } - if (hasFill) { - return absDistance <= r; + + get cy() { + return this.#cy; } - if (hasStroke) { - return ( - absDistance >= r + offset - strokeWidth && absDistance <= r + offset - ); + + set cy(cy: number) { + if (this.#cy !== cy) { + this.#cy = cy; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } } - return false; - } - getGeometryBounds() { - if (this.geometryBoundsDirtyFlag) { - this.geometryBoundsDirtyFlag = false; - this.geometryBounds = Circle.getGeometryBounds(this); + get r() { + return this.#r; } - return this.geometryBounds; - } - getRenderBounds() { - if (this.renderBoundsDirtyFlag) { - const { strokeWidth, strokeAlignment, cx, cy, r } = this; + set r(r: number) { + if (this.#r !== r) { + this.#r = r; + this.renderDirtyFlag = true; + this.geometryBoundsDirtyFlag = true; + this.renderBoundsDirtyFlag = true; + this.boundsDirtyFlag = true; + } + } + + containsPoint(x: number, y: number) { + const { + strokeWidth, + strokeAlignment, + cx, + cy, + r, + pointerEvents, + fill, + stroke, + } = this; + + const absDistance = distanceBetweenPoints(cx, cy, x, y); const offset = strokeOffset(strokeAlignment, strokeWidth); - this.renderBoundsDirtyFlag = false; - this.renderBounds = new AABB( - cx - r - offset, - cy - r - offset, - cx + r + offset, - cy + r + offset, + + const [hasFill, hasStroke] = isFillOrStrokeAffected( + pointerEvents, + fill, + stroke, ); + if (hasFill && hasStroke) { + return absDistance <= r + offset; + } + if (hasFill) { + return absDistance <= r; + } + if (hasStroke) { + return ( + absDistance >= r + offset - strokeWidth && absDistance <= r + offset + ); + } + return false; + } + + getGeometryBounds() { + if (this.geometryBoundsDirtyFlag) { + this.geometryBoundsDirtyFlag = false; + this.geometryBounds = Circle.getGeometryBounds(this); + } + return this.geometryBounds; + } + + getRenderBounds() { + if (this.renderBoundsDirtyFlag) { + const { strokeWidth, strokeAlignment, cx, cy, r } = this; + const offset = strokeOffset(strokeAlignment, strokeWidth); + this.renderBoundsDirtyFlag = false; + this.renderBounds = new AABB( + cx - r - offset, + cy - r - offset, + cx + r + offset, + cy + r + offset, + ); + } + return this.renderBounds; } - return this.renderBounds; - } + }; } diff --git a/packages/lesson_013/src/shapes/Rect.ts b/packages/lesson_013/src/shapes/Rect.ts index d6095f1..95b26f6 100644 --- a/packages/lesson_013/src/shapes/Rect.ts +++ b/packages/lesson_013/src/shapes/Rect.ts @@ -85,6 +85,8 @@ export function RectWrapper(Base: TBase) { #dropShadowOffsetY: number; #dropShadowBlurRadius: number; + onGeometryChanged?: () => void; + static getGeometryBounds( attributes: Partial>, ) { @@ -128,6 +130,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -141,6 +144,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -154,6 +158,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } @@ -167,6 +172,7 @@ export function RectWrapper(Base: TBase) { this.geometryBoundsDirtyFlag = true; this.renderBoundsDirtyFlag = true; this.boundsDirtyFlag = true; + this.onGeometryChanged?.(); } } diff --git a/packages/lesson_013/src/shapes/RoughCircle.ts b/packages/lesson_013/src/shapes/RoughCircle.ts new file mode 100644 index 0000000..d490b33 --- /dev/null +++ b/packages/lesson_013/src/shapes/RoughCircle.ts @@ -0,0 +1,22 @@ +import { CircleWrapper, CircleAttributes } from './Circle'; +import { generator } from '../utils'; +import { IRough, Rough } from './mixins/Rough'; +import { Shape } from './Shape'; + +export interface RoughCircleAttributes extends CircleAttributes, IRough {} + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-redeclare +export class RoughCircle extends Rough(CircleWrapper(Shape)) { + constructor(attributes: Partial = {}) { + super(attributes); + } + + // TODO: cx / cy / r also regenerates the drawable + + generateDrawable() { + const { cx, cy, r } = this; + + return generator.circle(cx, cy, r * 2, this.roughOptions); + } +} diff --git a/packages/lesson_013/src/shapes/RoughRect.ts b/packages/lesson_013/src/shapes/RoughRect.ts index 81e81c4..15e3b01 100644 --- a/packages/lesson_013/src/shapes/RoughRect.ts +++ b/packages/lesson_013/src/shapes/RoughRect.ts @@ -1,5 +1,5 @@ import { RectWrapper, RectAttributes } from './Rect'; -import { filterUndefined, generator } from '../utils'; +import { generator } from '../utils'; import { IRough, Rough } from './mixins/Rough'; import { Shape } from './Shape'; @@ -10,39 +10,17 @@ export interface RoughRectAttributes extends RectAttributes, IRough {} export class RoughRect extends Rough(RectWrapper(Shape)) { constructor(attributes: Partial = {}) { super(attributes); + + // x / y / width / height also regenerates the drawable + this.onGeometryChanged = () => { + this.geometryDirtyFlag = true; + this.generate(); + }; } generateDrawable() { - const { - x, - y, - width, - height, - fill, - stroke, - strokeWidth, - seed, - bowing, - roughness, - fillStyle, - fillWeight, - } = this; + const { x, y, width, height } = this; - return generator.rectangle( - x, - y, - width, - height, - filterUndefined({ - fill: fill as string, - stroke, - strokeWidth, - seed, - bowing, - roughness, - fillStyle, - fillWeight, - }), - ); + return generator.rectangle(x, y, width, height, this.roughOptions); } } diff --git a/packages/lesson_013/src/shapes/index.ts b/packages/lesson_013/src/shapes/index.ts index 30e3beb..5a8151c 100644 --- a/packages/lesson_013/src/shapes/index.ts +++ b/packages/lesson_013/src/shapes/index.ts @@ -7,4 +7,5 @@ export * from './Polyline'; export * from './Path'; export * from './Grid'; export * from './Group'; +export * from './RoughCircle'; export * from './RoughRect'; diff --git a/packages/lesson_013/src/shapes/mixins/Rough.ts b/packages/lesson_013/src/shapes/mixins/Rough.ts index c403cc1..a0ba1cf 100644 --- a/packages/lesson_013/src/shapes/mixins/Rough.ts +++ b/packages/lesson_013/src/shapes/mixins/Rough.ts @@ -1,6 +1,6 @@ import { Drawable, Options } from 'roughjs/bin/core'; import { GConstructor } from '.'; -import { parsePath } from '../../utils'; +import { filterUndefined, parsePath } from '../../utils'; export interface IRough extends Omit { @@ -139,6 +139,29 @@ export function Rough(Base: TBase) { } } + get roughOptions(): Options { + const { + fill, + stroke, + strokeWidth, + seed, + bowing, + roughness, + fillStyle, + fillWeight, + } = this; + return filterUndefined({ + fill: fill as string, + stroke, + strokeWidth, + seed, + bowing, + roughness, + fillStyle, + fillWeight, + }); + } + /** * generate rough shape */ diff --git a/packages/site/docs/guide/lesson-013.md b/packages/site/docs/guide/lesson-013.md index a1ef723..04875a0 100644 --- a/packages/site/docs/guide/lesson-013.md +++ b/packages/site/docs/guide/lesson-013.md @@ -26,8 +26,15 @@ $icCanvas = call(() => { ```js eval code=false inspector=false call(() => { - const { Canvas, Path, deserializeNode, fromSVGElement, TesselationMethod } = - Lesson13; + const { + Canvas, + Path, + RoughCircle, + RoughRect, + deserializeNode, + fromSVGElement, + TesselationMethod, + } = Lesson13; const stats = new Stats(); stats.showPanel(0); @@ -42,6 +49,29 @@ call(() => { $icCanvas.addEventListener('ic-ready', (e) => { const canvas = e.detail; + const circle = new RoughCircle({ + cx: 600, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag', + }); + canvas.appendChild(circle); + + const rect = new RoughRect({ + x: 550, + y: 200, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'dots', + }); + rect.width = 100; + rect.height = 50; + canvas.appendChild(rect); + fetch( '/Ghostscript_Tiger.svg', // '/photo-camera.svg', @@ -372,12 +402,216 @@ export interface PathAttributes extends ShapeAttributes { } ``` -## Hand-drawn style drawing {#sketchy} +## Hand-drawn style drawing {#hand-drawn-style-drawing} -[excalidraw] uses [rough] for hand-drawn style drawing. +[excalidraw] uses [rough] for hand-drawn style drawing. We don't need the actual Canvas2D or SVG based drawing functionality that rough provides by default, so using [RoughGenerator] is a better choice. ![rough.js](https://camo.githubusercontent.com/5d90838c20ae2cab9f295e3dd812800285c42e82d04787883c9d5acecaec85ed/68747470733a2f2f726f7567686a732e636f6d2f696d616765732f6361705f64656d6f2e706e67) +### Generate hand-drawn path definitions {#generate-rough-path-definitions} + +RoughGenerator provides generation methods for common shapes, using rectangles as an example: + +```ts +const generator = rough.generator(); +const rect = generator.rectangle(0, 0, 100, 100); +``` + +It generates a set of subPath-like structures for us based on the input parameters, called OpSet, which contains the `move` `lineTo` and `bcurveTo` operators. We can easily convert this to a command with an absolute path, then sample it and continue drawing with Polyline! + +```ts +import { AbsoluteArray } from '@antv/util'; +import { OpSet } from 'roughjs/bin/core'; + +export function opSet2Absolute(set: OpSet) { + const array = []; + set.ops.forEach(({ op, data }) => { + if (op === 'move') { + array.push(['M', data[0], data[1]]); + } else if (op === 'lineTo') { + array.push(['L', data[0], data[1]]); + } else if (op === 'bcurveTo') { + array.push([ + 'C', + data[0], + data[1], + data[2], + data[3], + data[4], + data[5], + ]); + } + }); + return array as AbsoluteArray; +} +``` + +### Rough Mixin {rough-mixin} + +We would like to reuse the non-hand-drawn version for these functions of the envelope box calculation and pickup for the following reasons: + +- This stylized rendering should only affect the rendering effect, it does not change its physical properties. +- A hand-drawn graphic actually consists of several sets of Paths, so it is a waste of performance to calculate the bounding box exactly. +- When picking up, it should be taken as a whole, and judging by the Paths will give wrong results, e.g. if the mouse is hovering inside the graphic, but is in the empty space between the lines, and thus is not inside the graphic. + So we create a new Mixin with all the parameters supported by rough such as `seed` `roughness` etc. and redraw it as soon as these parameters change: + +```ts +import { Drawable, Options } from 'roughjs/bin/core'; +import { GConstructor } from '.'; +import { parsePath } from '../../utils'; + +export interface IRough + extends Omit { + /** + * @see https://github.com/rough-stuff/rough/wiki#roughness + */ + roughness: Options['roughness']; +} +export function Rough(Base: TBase) { + abstract class Rough extends Base implements IRough { + get roughness() { + return this.#roughness; + } + set roughness(roughness: number) { + if (this.#roughness !== roughness) { + this.#roughness = roughness; + this.renderDirtyFlag = true; + this.generate(); + } + } + } +} +``` + +This way we can get hand-drawn effects by wrapping our already supported shapes in it. The way to use it is as follows, taking RoughRect as an example, which inherits from Rect: + +```ts +import { RectWrapper, RectAttributes } from './Rect'; + +export class RoughRect extends Rough(RectWrapper(Shape)) {} +``` + +### fillStyle solid {#fill-style-solid} + +To support the `fillStyle = 'solid'` case: + +```ts +SHAPE_DRAWCALL_CTORS.set(RoughRect, [ + ShadowRect, + Mesh, // fillStyle === 'solid' // [!code ++] + SmoothPolyline, // fill + SmoothPolyline, // stroke +]); +``` + +```js eval code=false +$icCanvas3 = call(() => { + return document.createElement('ic-canvas-lesson13'); +}); +``` + +```js eval code=false inspector=false +call(() => { + const { Canvas, RoughCircle } = Lesson13; + + const stats = new Stats(); + stats.showPanel(0); + const $stats = stats.dom; + $stats.style.position = 'absolute'; + $stats.style.left = '0px'; + $stats.style.top = '0px'; + + $icCanvas3.parentElement.style.position = 'relative'; + $icCanvas3.parentElement.appendChild($stats); + + const circle1 = new RoughCircle({ + cx: 100, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'dots', + }); + + const circle2 = new RoughCircle({ + cx: 200, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'hachure', + }); + + const circle3 = new RoughCircle({ + cx: 300, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag', + }); + + const circle4 = new RoughCircle({ + cx: 400, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'cross-hatch', + }); + + const circle5 = new RoughCircle({ + cx: 500, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'solid', + }); + + const circle6 = new RoughCircle({ + cx: 100, + cy: 200, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'dashed', + }); + + const circle7 = new RoughCircle({ + cx: 200, + cy: 200, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag-line', + }); + + $icCanvas3.addEventListener('ic-ready', (e) => { + const canvas = e.detail; + + canvas.appendChild(circle1); + canvas.appendChild(circle2); + canvas.appendChild(circle3); + canvas.appendChild(circle4); + canvas.appendChild(circle5); + canvas.appendChild(circle6); + canvas.appendChild(circle7); + }); + + $icCanvas3.addEventListener('ic-frame', (e) => { + stats.update(); + }); +}); +``` + ## Extended reading {#extended-reading} - [Rendering SVG Paths in WebGL] diff --git a/packages/site/docs/zh/guide/lesson-013.md b/packages/site/docs/zh/guide/lesson-013.md index 54a2194..61049cc 100644 --- a/packages/site/docs/zh/guide/lesson-013.md +++ b/packages/site/docs/zh/guide/lesson-013.md @@ -26,8 +26,15 @@ $icCanvas = call(() => { ```js eval code=false inspector=false call(() => { - const { Canvas, Path, deserializeNode, fromSVGElement, TesselationMethod } = - Lesson13; + const { + Canvas, + Path, + RoughCircle, + RoughRect, + deserializeNode, + fromSVGElement, + TesselationMethod, + } = Lesson13; const stats = new Stats(); stats.showPanel(0); @@ -42,6 +49,29 @@ call(() => { $icCanvas.addEventListener('ic-ready', (e) => { const canvas = e.detail; + const circle = new RoughCircle({ + cx: 600, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag', + }); + canvas.appendChild(circle); + + const rect = new RoughRect({ + x: 550, + y: 200, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'dots', + }); + rect.width = 100; + rect.height = 50; + canvas.appendChild(rect); + fetch( '/Ghostscript_Tiger.svg', // '/photo-camera.svg', @@ -376,7 +406,7 @@ export interface PathAttributes extends ShapeAttributes { 包围盒可以沿用上一节课针对折线的估计方式。 -## 手绘风格 {#hand-drawn-style} +## 手绘风格 {#hand-drawn-style-drawing} [excalidraw] 使用了 [rough] 进行手绘风格的绘制。我们并不需要 rough 默认提供的基于 Canvas2D 或 SVG 的实际绘制功能,使因此使用 [RoughGenerator] 是更好的选择。 @@ -487,7 +517,7 @@ $icCanvas3 = call(() => { ```js eval code=false inspector=false call(() => { - const { Canvas, RoughRect } = Lesson13; + const { Canvas, RoughCircle } = Lesson13; const stats = new Stats(); stats.showPanel(0); @@ -499,23 +529,86 @@ call(() => { $icCanvas3.parentElement.style.position = 'relative'; $icCanvas3.parentElement.appendChild($stats); - const rect1 = new RoughRect({ - x: 0, - y: 0, + const circle1 = new RoughCircle({ + cx: 100, + cy: 100, + r: 50, fill: 'black', strokeWidth: 2, stroke: 'red', - seed: 1, - roughness: 1, fillStyle: 'dots', }); - rect1.width = 100; - rect1.height = 100; + + const circle2 = new RoughCircle({ + cx: 200, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'hachure', + }); + + const circle3 = new RoughCircle({ + cx: 300, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag', + }); + + const circle4 = new RoughCircle({ + cx: 400, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'cross-hatch', + }); + + const circle5 = new RoughCircle({ + cx: 500, + cy: 100, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'solid', + }); + + const circle6 = new RoughCircle({ + cx: 100, + cy: 200, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'dashed', + }); + + const circle7 = new RoughCircle({ + cx: 200, + cy: 200, + r: 50, + fill: 'black', + strokeWidth: 2, + stroke: 'red', + fillStyle: 'zigzag-line', + }); $icCanvas3.addEventListener('ic-ready', (e) => { const canvas = e.detail; - canvas.appendChild(rect1); + canvas.appendChild(circle1); + canvas.appendChild(circle2); + canvas.appendChild(circle3); + canvas.appendChild(circle4); + canvas.appendChild(circle5); + canvas.appendChild(circle6); + canvas.appendChild(circle7); }); $icCanvas3.addEventListener('ic-frame', (e) => { diff --git a/screenshots/lesson13.png b/screenshots/lesson13.png index 23838c2..5051f93 100644 Binary files a/screenshots/lesson13.png and b/screenshots/lesson13.png differ