0.0.22 • Published 1 year ago

@ray-js/pencil-flow v0.0.22

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

Supports the rendering engine of the Tuya canvas (type="2d")

Feature

  • High performance and loosely coupled rendering architecture
  • Lightweight code volume
  • Supports canvas element management
  • Supports the canvas element event system
  • A complete group nesting system
  • Supports deformable clip clipping system
  • Built-in text, bitmaps, drawing objects and a variety of vector drawing objects
  • Built-in image loader

Performance

  • After testing, the Android mid-end machine can render 1000+ draggable graphics at the same time, and the fps is kept at 40+ when dragging graphics;

One minute to get started

Used in page or component

Declare the primary canvas and the secondary canvas elements for click event judgment in tyml

  • Attention ⚠️ :
      1. If multiple elements are required on a page, ensure that the canvas ids are different
      1. You are advised not to use more than three canvas components on the same page. Otherwise, performance problems may occur. If multiple canvas components are required, you can interact with each other to prevent multiple canvas components from existing at the same time

Draw an interactive circle with the following code example

<view class='ray-js_pencil_flow-container' style="position: relative;">
	<canvas class='ray-js_pencil_flow-canvas' canvas-id='{{ canvasId }}' style='width:{{width}}px;height:{{height}}px; border:1px solid rgb(173, 140, 90);' disable-scroll="true" type='2d'></canvas>
	<canvas class='ray-js_pencil_flow-hit-canvas' canvas-id='{{ hitCanvasId }}' style='width:{{width}}px;height:{{height}}px; position: absolute; left: 100000px;' disable-scroll="true" type='2d'></canvas>
</view>
.ray-js_pencil_flow-container {
  display: flex;
  justify-content: center;
  align-items: center;
}
.ray-js_pencil_flow-canvas {
  position: relative;
}
.ray-js_pencil_flow-hit-canvas {
  position: relative;
}
// index.rjs
import pencilFlow, { init } from '@ray-js/pencilFlow';
const { Stage, Group, Graphics, Rect, Circle, Bitmap, Text } = pencilFlow;

export default Render({
  renderCanvas(canvasId, hitCanvasId, pixelRatio) {
    init(canvasId, hitCanvasId, { pixelRatio }).then(({ stage }) => {
      const r = 50;
      const circle = new Circle(r, {
        gradientPoints: [
          { x: -r, y: r },
          { x: r, y: r },
        ],
        fillGradient: ['#ff0000', '#0000ff', '#ffff00'],
        id: 'circle',
      });
      circle.alpha = 1;
      circle.x = 200;
      circle.y = 200;
      circle.on('drag', function (event) {
        circle.x += event.dx;
        circle.y += event.dy;
        stage.update();
      });

      const circle1 = new Circle(50, {
        fillStyle: 'blue',
        id: 'circle1',
      });
      circle1.alpha = 0.5;
      circle1.x = 200;
      circle1.y = 50;

      circle1.on('drag', function (event) {
        circle1.x += event.dx;
        circle1.y += event.dy;
        stage.update();
      });

      const group = new Group();

      group.x = 20;
      group.y = 20;
      group.add(circle);
      group.add(circle1);

      stage.add(group);
      stage.update();
    });
  },
});
// index.js
import Render from './index.rjs';

const randomId = () => Math.random().toString(36).slice(2);

Component({
  properties: {
    width: {
      type: Number,
      value: 300,
    },
    height: {
      type: Number,
      value: 300,
    },
  },
  data: {
    canvasId: `canvas_${randomId()}`,
    hitCanvasId: `hitCanvas_${randomId()}`,
  },
  lifetimes: {
    attached() {
      this.render = new Render(this);
    },
    ready() {
      const { canvasId, hitCanvasId } = this.data;
      const { pixelRatio = 1 } = ty.getSystemInfoSync();
      this.render.renderCanvas(canvasId, hitCanvasId, pixelRatio);
    },
  },
});

Install

yarn add @ray-js/pencilFlow

Introduce and initialize the project in js

import pencilFlow, { init } from '@ray-js/pencilFlow';
const { Stage, Group, Graphics, Rect, Circle, Shape } = pencilFlow;

Built-in object

Group

Used for grouping, group can also be nested in group, and the properties of the parent container will be superimposed on the child properties, for example:

  • The x of the group is 100, the x of the bitmap in the group is 200, and the x of the final bitmap rendered to the stage is 300
  • The alpha of the group is 0.7, the alpha of the bitmap in the group is 0.6, and the alpha of the final bitmap rendered to the stage is 0.42
const group = new Group();
const rect = new Rect(100, 100, {
  fillStyle: 'red'
});
group.add(rect);
stage.add(group);
stage.update();

The group has the usual add and remove methods for adding and removing elements. The first to add will be drawn first, and all the later to add will be on top of the first to add.

Group Methods

add

Add object

group.add(child);
remove

Remove object

group.remove(child);
empty

Clearing child object

group.empty();
replace

Use an object instead of a sub-object

group.replace(current, pre);

Stage

The largest top-level container inherits from the group, so it has all the methods that the group has

Stage Methods

update

Any elements added to the Stage cannot be seen, and the update method needs to be executed. Modification of any element attributes requires executing stage.update() to update the rendering

const rect = new Rect(100, 100, {
  fillStyle: 'red'
});

rect.on('drag', (event) => {
  rect.x += event.dx;
  rect.y += event.dy;
  stage.update();
})
stage.add(rect);
// 渲染
stage.update();
setHitCanvas
  • The init method has been built in. If you use the init method, setHitCanvas will not be called.
  • Set up a simulated virtual canvas, accepting a parameter canvas object, used to calculate pixel-level touch event targets
const stage = new Stage(canvas, canvasWidth, canvasHeight, pixelRatio);
const hitCanvas = await getCanvasById('xxxId');
stage.setHitCanvas(hitCanvas);
getTextWidth

Get the width of the text to be rendered, two parameters, the first parameter is text: String, the text to be drawn, the second parameter is font: String, the style of the text to be set.

loadImage

stage has a built-in image loader that takes a parameter 'url: string' and returns a promise object.

The result of the promise execution is the image object, which is used for bitmap drawing.

const stage = new Stage(canvas, canvasWidth, canvasHeight, pixelRatio);
const imgObj2 = await stage.loadImage('../logo.png');

const bitmap = new Bitmap(imgObj2);
stage.add(bitmap);
stage.update();

Bitmap

bitmap accepts one parameter, an instance of an image object, and cannot use a url or local path. bitmap is synchronous and has no callback method.

const bitmap = new Bitmap(img);
stage.add(bitmap);
stage.update()

You can set the image crop display area, and other transform properties:

const bitmap = new Bitmap(img);
bitmap.x=50;
stage.add(bitmap);

const clipPath = new Graphics();
clipPath.rect(0, 0, 100, 200);
clipPath.x = 0;
clipPath.y = 50;
bitmap.clip(clipPath);
stage.add(bitmap);
stage.update()

Text

Text object

const textVal = 'This is the test text';
const fontSize = 18;
const text = new Text(textVal, {
  strokeStyle: 'red',
  size: fontSize,
  id: 'text',
});
getWidth

Get text width

stage.getTextWidth();

Graphics

Drawing object, used to draw graphics using the basic concatenated Canvas command

const graphics = new Graphics();
graphics
  .beginPath()
  .arc(0, 0, 10, 0, Math.PI * 2)
  .closePath()
  .fillStyle('#f4862c')
  .fill()
  .strokeStyle('black')
  .stroke();

graphics.x = 100;
graphics.y = 200;

stage.add(graphics);

Special note, if you perform graphics continuous drawing operations in a loop, be sure to add the clear() method, otherwise the path will be superimposed and your browser will be overwhelmed:

setInterval(function () {
  graphics
    .clear()
    .beginPath()
    .arc(0, 0, 10, 0, Math.PI * 2)
    .stroke();
}, 16);

Shape

Rect

const rect = new Rect(200, 100, {
  fillStyle: 'red'
});

Circle

const r = 50;
const circle = new Circle(r, {
  gradientPoints: [
    { x: -r, y: r },
    { x: r, y: r },
  ],
  fillGradient: ['#ff0000', '#0000ff', '#ffff00'],
  id: 'circle',
});

Ellipse

const width = 100;
const height = 50;
const ellipse = new Ellipse(width, height, {
  gradientPoints: [
    { x: 0, y: height / 2 },
    { x: width, y: height / 2 },
  ],
  fillGradient: ['#ff0000', 'pink', '#ffff00'],
  id: 'ellipse',
});

EquilateralPolygon

const num = 6;
const r = 50;
const equilateralPolygon1 = new EquilateralPolygon(num, r, {
  gradientPoints: [
    { x: -r, y: r },
    { x: r, y: r },
  ],
  fillGradient: ['red', 'green', 'blue'],
  id: 'equilateralPolygon1',
});

Polygon

const num = 6;
const r = 50;
const vertex = [
  [0, 0],
  [50, 0],
  [50, 50],
];
const polygon1 = new Polygon(vertex, {
  gradientPoints: [
    { x: vertex[0][0], y: vertex[0][1] },
    { x: vertex[2][0], y: vertex[2][1] },
  ],
  fillGradient: ['red', 'green', 'blue'],
  id: 'Polygon',
});

Ring

const r = 80;
const ring = new Ring(r, {
  strokeGradient: ['#4374f1', '#5ec7c7', '#f9d387', '#f1a88f', '#e78a86'],
  lineWidth: 15,
  strokeGradientPercent: 0.7,
  percent: 0.1,
  id: `ring`,
});

RoundedRect

const width = 100;
const height = 40;
const roundedRect2 = new RoundedRect(width, height, radius, {
  fillStyle: 'blue',
  id: 'Polygon2',
});

Stage

NameDescribe
stageThe stage you're on

Usage:

obj.stage;

property (valid for all graphics, text, group, and stage)

Transform

Attribute NameDescription
xHorizontal migration
yVertical migration
scaleXHorizontal scaling
scaleYVertical scaling
scaleSet or read scale x and scale y simultaneously
rotationrotate
skewXskew X
skewYskew Y
regXRotation base point X
regYRotation base point Y

Alpha

Attribute NameDescription
alphaTransparency of element

If alpha is set for both parent and child, multiply and stack.

Gradient

Attribute NameDescription
gradientPointsThe start and end points of a gradient line segment
fillGradientLinear fill gradient color values
strokeGradientLinear outline gradient color value

compositeOperation

Attribute NameDescription
compositeOperationOverlay mode where the source image is drawn onto the target image

Note that if compositeOperation is not defined here, it will search upwards and find the nearest parent container that defines compositeOperation as its own compositeOperation.

Shadow

Attribute nameDescription
shadowshadow

Usage:

const rect = new Rect(200, 100, {
  fillStyle: 'red'
});

rect.shadow = {
  color: '#42B035',
  offsetX: -5,
  offsetY: 5,
  blur: 10
};

stage.add(rect);
stage.update();

Methods

destroy

Destroy all elements in a group or stage (including yourself)

// Destroy all elements under the group
group.destroy();

// Destroy all elements under stage
stage.destroy();

empty

Empty all elements in a group or stage (excluding itself)

// Clear all elements under the group
group.empty();

// Empty all elements under stage
stage.empty();

Event

EventNameDescription
tapTouch the finger and leave immediately
touchstartFinger touch motion begins
touchmoveFingers touch and move
touchendFinger touch action is over
dragdrag

Events trigger down to the pixel level. If you want to use the rectangular area of the element as the click area, you need to set the hit box of the setting element

Clip

const stage = new Stage(canvas, canvasWidth, canvasHeight, pixelRatio);
const bitmap = new Bitmap(imgObj2);

const clipPath = new Graphics();
clipPath.arc(40, 40, 25, 0, Math.PI * 2);
bitmap.clip(clipPath);

stage.add(bitmap);

Use the following code to get the same effect:

const stage = new Stage(canvas, canvasWidth, canvasHeight, pixelRatio);
const bitmap = new Bitmap(imgObj2);

const clipPath = new Graphics();
clipPath.x = 40;
clipPath.y = 40;
clipPath.arc(0, 0, 25, 0, Math.PI * 2);
bitmap.clip(clipPath);

stage.add(bitmap);

The clipping area also supports all transform properties (x,y,scale x,scale y,rotation,skew x,skew y,reg x,reg y).

Custom object

Custom Shape

Custom shape inherits from shape:

class Sector extends Shape {
  [x: string]: any;
  option: optionInterface;

  constructor(radius: number, startAngle: number, endAngle: number, option: optionInterface) {
    super();

    this.option = option || {};
    this.radius = radius;
    this.startAngle = startAngle;
    this.endAngle = endAngle;
  }

  draw() {
    const {
      gradientPoints = [
        { x: -this.radius, y: this.radius },
        { x: this.radius, y: this.radius },
      ],
      fillGradient,
      strokeGradient,
      anticlockwise,
    } = this.option;

    this.beginPath()
      .moveTo(0, 0)
      .arc(0, 0, this.radius, this.startAngle, this.endAngle, anticlockwise)
      .closePath();

    if (this.option.fillStyle) {
      this.fillStyle(this.option.fillStyle);
      this.fill();
    }

    if (this.option.strokeStyle) {
      if (this.option.lineWidth !== undefined) {
        this.lineWidth(this.option.lineWidth);
      }
      this.strokeStyle(this.option.strokeStyle);
      this.stroke();
    }

    if (fillGradient && fillGradient.length) {
      const [point1, point2] = gradientPoints;
      const grad = this.createLinearGradient(point1.x, point1.y, point2.x, point2.y);
      if (this.option.fillGradient.length > 1) {
        for (let i = 0; i < this.option.fillGradient.length; i++) {
          grad.addColorStop(i / (this.option.fillGradient.length - 1), this.option.fillGradient[i]);
        }
      } else {
        grad.addColorStop(0, this.option.fillGradient[0]);
        grad.addColorStop(1, this.option.fillGradient[0]);
      }

      this.fillGradient();
      this.fill();
    }
    if (strokeGradient && strokeGradient.length) {
      const [point1, point2] = gradientPoints;
      const grad = this.createLinearGradient(point1.x, point1.y, point2.x, point2.y);

      if (this.option.strokeGradient.length > 1) {
        for (let i = 0; i < this.option.strokeGradient.length; i++) {
          grad.addColorStop(
            i / (this.option.strokeGradient.length - 1),
            this.option.strokeGradient[i]
          );
        }
      } else {
        grad.addColorStop(0, this.option.strokeGradient[0]);
        grad.addColorStop(1, this.option.strokeGradient[0]);
      }
      if (typeof this.option.lineWidth === 'number') {
        this.lineWidth(this.option.lineWidth);
      }
      this.strokeGradient();
      this.stroke();
    }
  }
}

Shape Usage:

const radius = 50;
const startAngle = 0;
const endAngle = (Math.PI * 2) / 4;
const sector = new Sector(radius, startAngle, endAngle, {
  fillStyle: 'blue',
  id: 'sector',
});
stage.add(sector)
stage.update()

Custom Element

A custom element inherits from a group:

class Button extends Group {
  constructor(stage, option) {
    super();
    const fontSize = option.fontSize || 20;
    const font = option.font || `${fontSize}px Arial`;
    const padding = 8;
    const textWidth = Math.floor(stage.getTextWidth(option.text, font)) + padding * 2;
    this.width = option.width || textWidth;
    this.height = option.height || 40;
    this.r = option.r || 8;
    this.roundedRect = new RoundedRect(this.width, this.height, this.r, {
      strokeStyle: option.color,
    });
    this.text = new Text(option.text, {
      strokeStyle: option.color,
      font: font,
      size: fontSize,
      id: 'text',
    });
    this.text.x = padding;
    this.text.y = 10;
    this.add(this.roundedRect, this.text);
  }
}

export default Button

Usage:

const customElement = new Button(stage, {
  text: 'Custom Element',
  id: 'customElement',
  color: 'red',
  fontSize: 20,
});

In general, it is recommended to use inheritance from groups for slightly more complex assemblies, which is easy to extend and manage their own internal components

Picture loader

The image loader returns a promise

const { loadImage } = pencilFlow;

// The canvas parameter is the obtained canvas 2d object instance
const imgObj = await loadImage('../logo.png', canvas);

// stage image loading method
const stage = new Stage(canvas, canvasWidth, canvasHeight, pixelRatio);
const imgObj2 = await stage.loadImage('../logo.png');

const bitmap = new bitMap(imgObj2);
stage.add(bitmap);
stage.update();

Matters needing attention

It has good support for gesture-related events, built-in drag and drop function, and supports two options of rectangular boundary and pixel-level boundary

The canvas initialization of the project adopts display setting width and height and ADAPTS pixel density by scaling to display HD

For more complex projects, it is recommended to develop components through classes, that is, each component is a class, and the class contains its own layout and update methods, so that you can develop highly reusable components, and then easy to maintain

Reference acknowledgements

License

MIT

0.0.22

1 year ago

0.0.22-beta-1

1 year ago

0.0.21-beta-1

1 year ago

0.0.20

1 year ago

0.0.20-beta-1

1 year ago

0.0.19-beta-1

1 year ago

0.0.18-beta-1

1 year ago

0.0.17-beta-1

1 year ago

0.0.16-beta-1

1 year ago