feat: implement group element render

This commit is contained in:
chenshenhai 2023-05-06 22:13:31 +08:00
parent d079ea4836
commit cc0f72c90b
16 changed files with 375 additions and 108 deletions

View file

@ -148,7 +148,7 @@ export class Calculator implements ViewCalculator {
index: -1,
element: null
};
for (let i = 0; i < data.elements.length; i++) {
for (let i = data.elements.length - 1; i >= 0; i--) {
const elem = data.elements[i];
if (this.isPointInElement(p, elem, scaleInfo)) {
result.index = i;

View file

@ -5,7 +5,7 @@ import type { AreaSize, ControllerStyle, ElementSizeController } from './types';
const wrapperColor = '#1973ba';
export function drawPointWrapper(ctx: ViewContext2D, elem: ElementSize, opts?: Omit<RendererDrawElementOptions, 'loader'>) {
export function drawPointWrapper(ctx: ViewContext2D, elem: ElementSize) {
const bw = 0;
const { x, y, w, h } = elem;
const { angle = 0 } = elem;
@ -35,7 +35,7 @@ export function drawPointWrapper(ctx: ViewContext2D, elem: ElementSize, opts?: O
});
}
export function drawHoverWrapper(ctx: ViewContext2D, elem: ElementSize, opts?: Omit<RendererDrawElementOptions, 'loader'>) {
export function drawHoverWrapper(ctx: ViewContext2D, elem: ElementSize) {
const bw = 0;
const { x, y, w, h } = elem;
const { angle = 0 } = elem;
@ -85,7 +85,7 @@ function drawController(ctx: ViewContext2D, style: ControllerStyle) {
export function drawElementControllers(
ctx: ViewContext2D,
elem: ElementSize,
opts: Omit<RendererDrawElementOptions, 'loader'> & { sizeControllers: ElementSizeController }
opts: Omit<RendererDrawElementOptions, 'loader' | 'parentElementSize'> & { sizeControllers: ElementSizeController }
) {
const bw = 0;
const { x, y, w, h } = elem;

View file

@ -257,13 +257,13 @@ export const MiddlewareSelector: BoardMiddleware = (opts) => {
const drawOpts = { calculator, scaleInfo, viewSize };
if (hoverElement && actionType !== 'drag') {
const hoverElemSize = calculator.elementSize(hoverElement, scaleInfo);
drawHoverWrapper(helperContext, hoverElemSize, drawOpts);
drawHoverWrapper(helperContext, hoverElemSize);
}
if (elem && ['select', 'drag', 'resize'].includes(actionType)) {
const selectedElemSize = calculator.elementSize(elem, scaleInfo);
const sizeControllers = calcElementControllerStyle(selectedElemSize);
drawPointWrapper(helperContext, selectedElemSize, drawOpts);
drawPointWrapper(helperContext, selectedElemSize);
drawElementControllers(helperContext, selectedElemSize, { ...drawOpts, sizeControllers });
} else if (actionType === 'area' && areaStart && areaEnd) {
drawArea(helperContext, { start: areaStart, end: areaEnd });

View file

@ -432,6 +432,7 @@ export function calcSelectedElementsArea(
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
const ves = rotateElementVertexes(elemSize);
if (ves.length === 4) {
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];

View file

@ -145,7 +145,6 @@ const data: Data = {
}
},
{
name: 'html-001',
uuid: 'xxx-0012',
x: 400,
y: 200,
@ -196,11 +195,160 @@ const data: Data = {
<div class="btn-box">
<button class="btn btn-primary">
<span>Button Primary</span>
</button>
</button>
</div>
</div>
`
}
},
{
uuid: 'group-001',
x: 400,
y: 400,
w: 100,
h: 100,
type: 'group',
desc: {
bgColor: '#1f1f1f',
children: [
{
uuid: 'group-001-0014',
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: 'group-001-0015',
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: 'group-001-0016',
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: 'group-001-0017',
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: 'group-001-0018',
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
},
{
uuid: 'group-0013',
x: 550,
y: 100,
w: 173.20508075688775,
// w: 100,
h: 100,
angle: 30,
type: 'group',
desc: {
children: [
{
uuid: 'group-002-014',
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: 'group-002-0015',
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: 'group-002-0016',
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: 'group-002-0017',
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: 'group-002-0018',
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
},
{
uuid: 'xxxx-0017',
type: 'image',
x: 100,
y: 400,
w: 100,
h: 100,
angle: 30,
desc: {
src: './images/lena.png?v=0017'
}
}
]
};

View file

@ -1,87 +1,81 @@
import { ViewContext2D, Element } from '@idraw/types';
import { is, istype, isColorStr, rotateElement } from '@idraw/util';
import { ViewContext2D, Element, ElementType } from '@idraw/types';
import { is, istype, isColorStr } from '@idraw/util';
export function clearContext(ctx: ViewContext2D) {
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
ctx.setLineDash([]);
ctx.globalAlpha = 1;
ctx.shadowColor = '#00000000';
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
}
// export function clearContext(ctx: ViewContext2D) {
// ctx.fillStyle = '#000000';
// ctx.strokeStyle = '#000000';
// ctx.setLineDash([]);
// ctx.globalAlpha = 1;
// ctx.shadowColor = '#00000000';
// ctx.shadowOffsetX = 0;
// ctx.shadowOffsetY = 0;
// ctx.shadowBlur = 0;
// }
export function drawBox(ctx: ViewContext2D, elem: Element<'text' | 'rect'>, pattern: string | CanvasPattern | null): void {
clearContext(ctx);
export function drawBox(ctx: ViewContext2D, elem: Element<ElementType>, pattern?: string | CanvasPattern | null): void {
drawBoxBorder(ctx, elem);
clearContext(ctx);
rotateElement(ctx, elem, () => {
const { x, y, w, h } = elem;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if (typeof pattern === 'string') {
ctx.fillStyle = pattern;
} else if (['CanvasPattern'].includes(istype.type(pattern))) {
ctx.fillStyle = pattern as CanvasPattern;
}
ctx.fill();
});
const { x, y, w, h } = elem;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if (typeof pattern === 'string') {
ctx.fillStyle = pattern;
} else if (['CanvasPattern'].includes(istype.type(pattern))) {
ctx.fillStyle = pattern as CanvasPattern;
}
ctx.fill();
}
export function drawBoxBorder(ctx: ViewContext2D, elem: Element<'text' | 'rect'>): void {
clearContext(ctx);
rotateElement(ctx, elem, () => {
if (!(elem.desc.borderWidth && elem.desc.borderWidth > 0)) {
return;
}
const bw = elem.desc.borderWidth;
let borderColor = '#000000';
if (isColorStr(elem.desc.borderColor) === true) {
borderColor = elem.desc.borderColor as string;
}
const x = elem.x - bw / 2;
const y = elem.y - bw / 2;
const w = elem.w + bw;
const h = elem.h + bw;
export function drawBoxBorder(ctx: ViewContext2D, elem: Element<ElementType>): void {
if (!(elem.desc.borderWidth && elem.desc.borderWidth > 0)) {
return;
}
const bw = elem.desc.borderWidth;
let borderColor = '#000000';
if (isColorStr(elem.desc.borderColor) === true) {
borderColor = elem.desc.borderColor as string;
}
const x = elem.x - bw / 2;
const y = elem.y - bw / 2;
const w = elem.w + bw;
const h = elem.h + bw;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (r < w / 2 && r < h / 2) {
r = r + bw / 2;
}
const { desc } = elem;
if (desc.shadowColor !== undefined && isColorStr(desc.shadowColor)) {
ctx.shadowColor = desc.shadowColor;
}
if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) {
ctx.shadowOffsetX = desc.shadowOffsetX;
}
if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
ctx.shadowOffsetY = desc.shadowOffsetY;
}
if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) {
ctx.shadowBlur = desc.shadowBlur;
}
ctx.beginPath();
ctx.lineWidth = bw;
ctx.strokeStyle = borderColor;
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
});
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (r < w / 2 && r < h / 2) {
r = r + bw / 2;
}
const { desc } = elem;
if (desc.shadowColor !== undefined && isColorStr(desc.shadowColor)) {
ctx.shadowColor = desc.shadowColor;
}
if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) {
ctx.shadowOffsetX = desc.shadowOffsetX;
}
if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
ctx.shadowOffsetY = desc.shadowOffsetY;
}
if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) {
ctx.shadowBlur = desc.shadowBlur;
}
ctx.beginPath();
ctx.lineWidth = bw;
ctx.strokeStyle = borderColor;
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
}

View file

@ -5,6 +5,7 @@ import { drawImage } from './image';
import { drawText } from './text';
import { drawSVG } from './svg';
import { drawHTML } from './html';
import { drawGroup } from './group';
export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts: RendererDrawElementOptions) {
try {
@ -33,6 +34,10 @@ export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts
drawHTML(ctx, elem as Element<'html'>, opts);
break;
}
case 'group': {
drawGroup(ctx, elem as Element<'group'>, opts);
break;
}
default: {
break;
}
@ -43,7 +48,7 @@ export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts
}
export function drawElementList(ctx: ViewContext2D, elements: Data['elements'], opts: RendererDrawElementOptions) {
for (let i = elements.length - 1; i >= 0; i--) {
for (let i = 0; i < elements.length; i++) {
const elem = elements[i];
if (!opts.calculator.isElementInView(elem, opts.scaleInfo, opts.viewSize)) {
continue;

View file

@ -0,0 +1,95 @@
import type { Element, ElementType, ElementSize, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
import { rotateElement } from '@idraw/util';
import { drawCircle } from './circle';
import { drawRect } from './rect';
import { drawImage } from './image';
import { drawText } from './text';
import { drawSVG } from './svg';
import { drawHTML } from './html';
import { drawBox } from './base';
export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts: RendererDrawElementOptions) {
try {
switch (elem.type) {
case 'rect': {
drawRect(ctx, elem as Element<'rect'>, opts);
break;
}
case 'circle': {
drawCircle(ctx, elem as Element<'circle'>, opts);
break;
}
case 'text': {
drawText(ctx, elem as Element<'text'>, opts);
break;
}
case 'image': {
drawImage(ctx, elem as Element<'image'>, opts);
break;
}
case 'svg': {
drawSVG(ctx, elem as Element<'svg'>, opts);
break;
}
case 'html': {
drawHTML(ctx, elem as Element<'html'>, opts);
break;
}
default: {
break;
}
}
} catch (err) {
console.error(err);
}
}
export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: RendererDrawElementOptions) {
const { calculator, scaleInfo } = opts;
const { x, y, w, h, angle } = calculator.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h, angle: elem.angle }, scaleInfo);
rotateElement(ctx, { x, y, w, h, angle }, () => {
drawBox(ctx, elem);
if (Array.isArray(elem.desc.children)) {
const { parentElementSize: parentSize } = opts;
const newParentSize: ElementSize = {
x: parentSize.x + elem.x,
y: parentSize.y + elem.y,
w: elem.w || parentSize.w,
h: elem.h || parentSize.h,
angle: elem.angle
};
const { calculator } = opts;
ctx.save();
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.closePath();
ctx.clip();
for (let i = 0; i < elem.desc.children.length; i++) {
let child = elem.desc.children[i];
child = {
...child,
...{
x: newParentSize.x + child.x,
y: newParentSize.y + child.y
}
};
if (!calculator.isElementInView(child, opts.scaleInfo, opts.viewSize)) {
continue;
}
try {
drawElement(ctx, child, opts);
} catch (err) {
console.error(err);
}
}
ctx.restore();
}
});
}

View file

@ -5,7 +5,7 @@ export function drawRect(ctx: ViewContext2D, elem: Element<'rect'>, opts: Render
// const { desc } = elem;
const { calculator, scaleInfo } = opts;
const { x, y, w, h, angle } = calculator.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, scaleInfo);
const { x, y, w, h, angle } = calculator.elementSize(elem, scaleInfo);
rotateElement(ctx, { x, y, w, h, angle }, () => {
let r: number = (elem.desc.borderRadius || 0) * scaleInfo.scale;
r = Math.min(r, w / 2, h / 2);

View file

@ -1,12 +1,11 @@
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
import { rotateElement } from '@idraw/util';
import { is, isColorStr } from '@idraw/util';
import { clearContext, drawBox } from './base';
import { drawBox } from './base';
export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) {
clearContext(ctx);
drawBox(ctx, elem, elem.desc.bgColor || 'transparent');
rotateElement(ctx, elem, () => {
drawBox(ctx, elem, elem.desc.bgColor || 'transparent');
const desc: Element<'text'>['desc'] = {
...{
fontSize: 12,
@ -103,7 +102,6 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
}
ctx.fillText(line.text, _x, _y + fontHeight * i);
});
clearContext(ctx);
}
// draw text stroke

View file

@ -1,4 +1,4 @@
import { EventEmitter, createOffscreenContext2D } from '@idraw/util';
import { EventEmitter } from '@idraw/util';
import { drawElementList } from './draw';
import { Loader } from './loader';
import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDrawOptions } from '@idraw/types';
@ -6,17 +6,17 @@ import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDr
export class Renderer extends EventEmitter<RendererEventMap> implements BoardRenderer {
private _opts: RendererOptions;
private _loader: Loader = new Loader();
private _draftContextTop: CanvasRenderingContext2D;
private _draftContextMiddle: CanvasRenderingContext2D;
private _draftContextBottom: CanvasRenderingContext2D;
// private _draftContextTop: CanvasRenderingContext2D;
// private _draftContextMiddle: CanvasRenderingContext2D;
// private _draftContextBottom: CanvasRenderingContext2D;
constructor(opts: RendererOptions) {
super();
this._opts = opts;
const { width, height } = this._opts.viewContent.viewContext.canvas;
this._draftContextTop = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._draftContextMiddle = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._draftContextBottom = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
// const { width, height } = this._opts.viewContent.viewContext.canvas;
// this._draftContextTop = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
// this._draftContextMiddle = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
// this._draftContextBottom = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._init();
}
@ -40,7 +40,18 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
const { calculator } = this._opts;
const { viewContext } = this._opts.viewContent;
viewContext.clearRect(0, 0, viewContext.canvas.width, viewContext.canvas.height);
drawElementList(viewContext, data.elements, { loader, calculator, ...opts });
const parentElementSize = {
x: 0,
y: 0,
w: opts.viewSize.width,
h: opts.viewSize.height
};
drawElementList(viewContext, data.elements, {
loader,
calculator,
parentElementSize,
...opts
});
}
scale(num: number) {

View file

@ -54,4 +54,6 @@ export interface ViewContext2D {
shadowOffsetY: number;
ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean;
clip(fillRule?: CanvasFillRule): void;
clip(path: Path2D, fillRule?: CanvasFillRule): void;
}

View file

@ -26,7 +26,7 @@ interface ElemenTextDesc extends ElementBaseDesc {
color: string;
fontSize: number;
lineHeight?: number;
fontWeight?: 'bold' | '';
fontWeight?: 'bold' | string;
fontFamily?: string;
textAlign?: 'center' | 'left' | 'right';
verticalAlign?: 'middle' | 'top' | 'bottom';
@ -58,6 +58,10 @@ interface ElementSVGDesc extends ElementBaseDesc {
svg: string;
}
interface ElementGroupDesc extends ElementBaseDesc {
children: Element<ElementType>[];
}
interface ElementDescMap {
rect: ElementRectDesc;
circle: ElementCircleDesc;
@ -65,9 +69,10 @@ interface ElementDescMap {
image: ElementImageDesc;
html: ElementHTMLDesc;
svg: ElementSVGDesc;
group: ElementGroupDesc;
}
export type ElementType = 'text' | 'rect' | 'circle' | 'image' | 'svg' | 'html';
export type ElementType = 'text' | 'rect' | 'circle' | 'image' | 'svg' | 'html' | 'group';
export interface ElementOperation {
lock?: boolean;

View file

@ -1,5 +1,5 @@
import type { ViewContent, ViewScaleInfo, ViewCalculator, ViewSizeInfo } from './view';
import type { Element } from './element';
import type { Element, ElementSize } from './element';
import type { LoaderEventMap, LoadElementType, LoadContent } from './loader';
import type { UtilEventEmitter } from './util';
import type { StoreSharer } from './store';
@ -31,4 +31,6 @@ export interface RendererDrawOptions {
export interface RendererDrawElementOptions extends RendererDrawOptions {
loader: RendererLoader;
calculator: ViewCalculator;
scaleInfo: ViewScaleInfo;
parentElementSize: ElementSize;
}

View file

@ -272,4 +272,10 @@ export class Context2D implements ViewContext2D {
isPointInPath(x: number, y: number) {
return this._ctx.isPointInPath(this.$doPixelRatio(x), this.$doPixelRatio(y));
}
// clip(fillRule?: CanvasFillRule): void;
// clip(path: Path2D, fillRule?: CanvasFillRule): void;
clip(...args: [fillRule?: CanvasFillRule | undefined] | [path: Path2D, fillRule?: CanvasFillRule | undefined]) {
return this._ctx.clip(...(args as any[]));
}
}

View file

@ -85,9 +85,9 @@ function calcLineRadian(center: PointSize, p: PointSize): number {
} else if (x > 0 && y > 0) {
return Math.PI - Math.atan(Math.abs(x) / Math.abs(y));
} else if (x < 0 && y > 0) {
return Math.PI + Math.atan(Math.abs(y) / Math.abs(x));
return Math.PI + Math.atan(Math.abs(x) / Math.abs(y));
} else if (x < 0 && y < 0) {
return 2 * Math.PI - Math.atan(Math.abs(y) / Math.abs(x));
return 2 * Math.PI - Math.atan(Math.abs(x) / Math.abs(y));
}
return 0;