Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-08-26 09:31:34 +08:00
parent 9b8c4bfab6
commit 394eb0285d
34137 changed files with 3589424 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
// CompoundPath to improve performance
import Path from './Path';
import PathProxy from '../core/PathProxy';
export interface CompoundPathShape {
paths: Path[]
}
export default class CompoundPath extends Path {
type = 'compound'
shape: CompoundPathShape
private _updatePathDirty() {
const paths = this.shape.paths;
let dirtyPath = this.shapeChanged();
for (let i = 0; i < paths.length; i++) {
// Mark as dirty if any subpath is dirty
dirtyPath = dirtyPath || paths[i].shapeChanged();
}
if (dirtyPath) {
this.dirtyShape();
}
}
beforeBrush() {
this._updatePathDirty();
const paths = this.shape.paths || [];
const scale = this.getGlobalScale();
// Update path scale
for (let i = 0; i < paths.length; i++) {
if (!paths[i].path) {
paths[i].createPathProxy();
}
paths[i].path.setScale(scale[0], scale[1], paths[i].segmentIgnoreThreshold);
}
}
buildPath(ctx: PathProxy | CanvasRenderingContext2D, shape: CompoundPathShape) {
const paths = shape.paths || [];
for (let i = 0; i < paths.length; i++) {
paths[i].buildPath(ctx, paths[i].shape, true);
}
}
afterBrush() {
const paths = this.shape.paths || [];
for (let i = 0; i < paths.length; i++) {
paths[i].pathUpdated();
}
}
getBoundingRect() {
this._updatePathDirty.call(this);
return Path.prototype.getBoundingRect.call(this);
}
}
+624
View File
@@ -0,0 +1,624 @@
/**
* Base class of all displayable graphic objects
*/
import Element, {ElementProps, ElementStatePropNames, ElementAnimateConfig, ElementCommonState} from '../Element';
import BoundingRect from '../core/BoundingRect';
import { PropType, Dictionary, MapToType } from '../core/types';
import Path from './Path';
import { keys, extend, createObject } from '../core/util';
import Animator from '../animation/Animator';
import { REDRAW_BIT, STYLE_CHANGED_BIT } from './constants';
// type CalculateTextPositionResult = ReturnType<typeof calculateTextPosition>
const STYLE_MAGIC_KEY = '__zr_style_' + Math.round((Math.random() * 10));
export interface CommonStyleProps {
shadowBlur?: number
shadowOffsetX?: number
shadowOffsetY?: number
shadowColor?: string
opacity?: number
/**
* https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
*/
blend?: string
}
export const DEFAULT_COMMON_STYLE: CommonStyleProps = {
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: '#000',
opacity: 1,
blend: 'source-over'
};
export const DEFAULT_COMMON_ANIMATION_PROPS: MapToType<DisplayableProps, boolean> = {
style: {
shadowBlur: true,
shadowOffsetX: true,
shadowOffsetY: true,
shadowColor: true,
opacity: true
}
};
(DEFAULT_COMMON_STYLE as any)[STYLE_MAGIC_KEY] = true;
export interface DisplayableProps extends ElementProps {
style?: Dictionary<any>
zlevel?: number
z?: number
z2?: number
culling?: boolean
// TODO list all cursors
cursor?: string
rectHover?: boolean
progressive?: boolean
incremental?: boolean
ignoreCoarsePointer?: boolean
batch?: boolean
invisible?: boolean
}
type DisplayableKey = keyof DisplayableProps
type DisplayablePropertyType = PropType<DisplayableProps, DisplayableKey>
export type DisplayableStatePropNames = ElementStatePropNames | 'style' | 'z' | 'z2' | 'invisible';
export type DisplayableState = Pick<DisplayableProps, DisplayableStatePropNames> & ElementCommonState;
const PRIMARY_STATES_KEYS = ['z', 'z2', 'invisible'] as const;
const PRIMARY_STATES_KEYS_IN_HOVER_LAYER = ['invisible'] as const;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Displayable<Props extends DisplayableProps = DisplayableProps> {
animate(key?: '', loop?: boolean): Animator<this>
animate(key: 'style', loop?: boolean): Animator<this['style']>
getState(stateName: string): DisplayableState
ensureState(stateName: string): DisplayableState
states: Dictionary<DisplayableState>
stateProxy: (stateName: string) => DisplayableState
}
class Displayable<Props extends DisplayableProps = DisplayableProps> extends Element<Props> {
/**
* Whether the displayable object is visible. when it is true, the displayable object
* is not drawn, but the mouse event can still trigger the object.
*/
invisible: boolean
z: number
z2: number
/**
* The z level determines the displayable object can be drawn in which layer canvas.
*/
zlevel: number
/**
* If enable culling
*/
culling: boolean
/**
* Mouse cursor when hovered
*/
cursor: string
/**
* If hover area is bounding rect
*/
rectHover: boolean
/**
* For increamental rendering
*/
incremental: boolean
/**
* Never increase to target size
*/
ignoreCoarsePointer?: boolean
style: Dictionary<any>
protected _normalState: DisplayableState
protected _rect: BoundingRect
protected _paintRect: BoundingRect
protected _prevPaintRect: BoundingRect
dirtyRectTolerance: number
/************* Properties will be inejected in other modules. *******************/
// @deprecated.
useHoverLayer?: boolean
__hoverStyle?: CommonStyleProps
// TODO use WeakMap?
// Shapes for cascade clipping.
// Can only be `null`/`undefined` or an non-empty array, MUST NOT be an empty array.
// because it is easy to only using null to check whether clipPaths changed.
__clipPaths?: Path[]
// FOR CANVAS PAINTER
__canvasFillGradient: CanvasGradient
__canvasStrokeGradient: CanvasGradient
__canvasFillPattern: CanvasPattern
__canvasStrokePattern: CanvasPattern
// FOR SVG PAINTER
__svgEl: SVGElement
constructor(props?: Props) {
super(props);
}
protected _init(props?: Props) {
// Init default properties
const keysArr = keys(props);
for (let i = 0; i < keysArr.length; i++) {
const key = keysArr[i];
if (key === 'style') {
this.useStyle(props[key] as Props['style']);
}
else {
super.attrKV(key as any, props[key]);
}
}
// Give a empty style
if (!this.style) {
this.useStyle({});
}
}
// Hook provided to developers.
beforeBrush() {}
afterBrush() {}
// Hook provided to inherited classes.
// Executed between beforeBrush / afterBrush
innerBeforeBrush() {}
innerAfterBrush() {}
shouldBePainted(
viewWidth: number,
viewHeight: number,
considerClipPath: boolean,
considerAncestors: boolean
) {
const m = this.transform;
if (
this.ignore
// Ignore invisible element
|| this.invisible
// Ignore transparent element
|| this.style.opacity === 0
// Ignore culled element
|| (this.culling
&& isDisplayableCulled(this, viewWidth, viewHeight)
)
// Ignore scale 0 element, in some environment like node-canvas
// Draw a scale 0 element can cause all following draw wrong
// And setTransform with scale 0 will cause set back transform failed.
|| (m && !m[0] && !m[3])
) {
return false;
}
if (considerClipPath && this.__clipPaths) {
for (let i = 0; i < this.__clipPaths.length; ++i) {
if (this.__clipPaths[i].isZeroArea()) {
return false;
}
}
}
if (considerAncestors && this.parent) {
let parent = this.parent;
while (parent) {
if (parent.ignore) {
return false;
}
parent = parent.parent;
}
}
return true;
}
/**
* If displayable element contain coord x, y
*/
contain(x: number, y: number) {
return this.rectContain(x, y);
}
traverse<Context>(
cb: (this: Context, el: this) => void,
context?: Context
) {
cb.call(context, this);
}
/**
* If bounding rect of element contain coord x, y
*/
rectContain(x: number, y: number) {
const coord = this.transformCoordToLocal(x, y);
const rect = this.getBoundingRect();
return rect.contain(coord[0], coord[1]);
}
getPaintRect(): BoundingRect {
let rect = this._paintRect;
if (!this._paintRect || this.__dirty) {
const transform = this.transform;
const elRect = this.getBoundingRect();
const style = this.style;
const shadowSize = style.shadowBlur || 0;
const shadowOffsetX = style.shadowOffsetX || 0;
const shadowOffsetY = style.shadowOffsetY || 0;
rect = this._paintRect || (this._paintRect = new BoundingRect(0, 0, 0, 0));
if (transform) {
BoundingRect.applyTransform(rect, elRect, transform);
}
else {
rect.copy(elRect);
}
if (shadowSize || shadowOffsetX || shadowOffsetY) {
rect.width += shadowSize * 2 + Math.abs(shadowOffsetX);
rect.height += shadowSize * 2 + Math.abs(shadowOffsetY);
rect.x = Math.min(rect.x, rect.x + shadowOffsetX - shadowSize);
rect.y = Math.min(rect.y, rect.y + shadowOffsetY - shadowSize);
}
// For the accuracy tolerance of text height or line joint point
const tolerance = this.dirtyRectTolerance;
if (!rect.isZero()) {
rect.x = Math.floor(rect.x - tolerance);
rect.y = Math.floor(rect.y - tolerance);
rect.width = Math.ceil(rect.width + 1 + tolerance * 2);
rect.height = Math.ceil(rect.height + 1 + tolerance * 2);
}
}
return rect;
}
setPrevPaintRect(paintRect: BoundingRect) {
if (paintRect) {
this._prevPaintRect = this._prevPaintRect || new BoundingRect(0, 0, 0, 0);
this._prevPaintRect.copy(paintRect);
}
else {
this._prevPaintRect = null;
}
}
getPrevPaintRect(): BoundingRect {
return this._prevPaintRect;
}
/**
* Alias for animate('style')
* @param loop
*/
animateStyle(loop: boolean) {
return this.animate('style', loop);
}
// Override updateDuringAnimation
updateDuringAnimation(targetKey: string) {
if (targetKey === 'style') {
this.dirtyStyle();
}
else {
this.markRedraw();
}
}
attrKV(key: DisplayableKey, value: DisplayablePropertyType) {
if (key !== 'style') {
super.attrKV(key as keyof DisplayableProps, value);
}
else {
if (!this.style) {
this.useStyle(value as Dictionary<any>);
}
else {
this.setStyle(value as Dictionary<any>);
}
}
}
setStyle(obj: Props['style']): this
setStyle<T extends keyof Props['style']>(obj: T, value: Props['style'][T]): this
setStyle(keyOrObj: keyof Props['style'] | Props['style'], value?: unknown): this {
if (typeof keyOrObj === 'string') {
this.style[keyOrObj] = value;
}
else {
extend(this.style, keyOrObj as Props['style']);
}
this.dirtyStyle();
return this;
}
// getDefaultStyleValue<T extends keyof Props['style']>(key: T): Props['style'][T] {
// // Default value is on the prototype.
// return this.style.prototype[key];
// }
dirtyStyle(notRedraw?: boolean) {
if (!notRedraw) {
this.markRedraw();
}
this.__dirty |= STYLE_CHANGED_BIT;
// Clear bounding rect.
if (this._rect) {
this._rect = null;
}
}
dirty() {
this.dirtyStyle();
}
/**
* Is style changed. Used with dirtyStyle.
*/
styleChanged() {
return !!(this.__dirty & STYLE_CHANGED_BIT);
}
/**
* Mark style updated. Only useful when style is used for caching. Like in the text.
*/
styleUpdated() {
this.__dirty &= ~STYLE_CHANGED_BIT;
}
/**
* Create a style object with default values in it's prototype.
*/
createStyle(obj?: Props['style']) {
return createObject(DEFAULT_COMMON_STYLE, obj);
}
/**
* Replace style property.
* It will create a new style if given obj is not a valid style object.
*/
// PENDING should not createStyle if it's an style object.
useStyle(obj: Props['style']) {
if (!obj[STYLE_MAGIC_KEY]) {
obj = this.createStyle(obj);
}
if (this.__inHover) {
this.__hoverStyle = obj; // Not affect exists style.
}
else {
this.style = obj;
}
this.dirtyStyle();
}
/**
* Determine if an object is a valid style object.
* Which means it is created by `createStyle.`
*
* A valid style object will have all default values in it's prototype.
* To avoid get null/undefined values.
*/
isStyleObject(obj: Props['style']) {
return obj[STYLE_MAGIC_KEY];
}
protected _innerSaveToNormal(toState: DisplayableState) {
super._innerSaveToNormal(toState);
const normalState = this._normalState;
if (toState.style && !normalState.style) {
// Clone style object.
// TODO: Only save changed style.
normalState.style = this._mergeStyle(this.createStyle(), this.style);
}
this._savePrimaryToNormal(toState, normalState, PRIMARY_STATES_KEYS);
}
protected _applyStateObj(
stateName: string,
state: DisplayableState,
normalState: DisplayableState,
keepCurrentStates: boolean,
transition: boolean,
animationCfg: ElementAnimateConfig
) {
super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
const needsRestoreToNormal = !(state && keepCurrentStates);
let targetStyle: Props['style'];
if (state && state.style) {
// Only animate changed properties.
if (transition) {
if (keepCurrentStates) {
targetStyle = state.style;
}
else {
targetStyle = this._mergeStyle(this.createStyle(), normalState.style);
this._mergeStyle(targetStyle, state.style);
}
}
else {
targetStyle = this._mergeStyle(
this.createStyle(),
keepCurrentStates ? this.style : normalState.style
);
this._mergeStyle(targetStyle, state.style);
}
}
else if (needsRestoreToNormal) {
targetStyle = normalState.style;
}
if (targetStyle) {
if (transition) {
// Clone a new style. Not affect the original one.
const sourceStyle = this.style;
this.style = this.createStyle(needsRestoreToNormal ? {} : sourceStyle);
// const sourceStyle = this.style = this.createStyle(this.style);
if (needsRestoreToNormal) {
const changedKeys = keys(sourceStyle);
for (let i = 0; i < changedKeys.length; i++) {
const key = changedKeys[i];
if (key in targetStyle) { // Not use `key == null` because == null may means no stroke/fill.
// Pick out from prototype. Or the property won't be animated.
(targetStyle as any)[key] = targetStyle[key];
// Omit the property has no default value.
(this.style as any)[key] = sourceStyle[key];
}
}
}
// If states is switched twice in ONE FRAME, for example:
// one property(for example shadowBlur) changed from default value to a specifed value,
// then switched back in immediately. this.style may don't set this property yet when switching back.
// It won't treat it as an changed property when switching back. And it won't be animated.
// So here we make sure the properties will be animated from default value to a specifed value are set.
const targetKeys = keys(targetStyle);
for (let i = 0; i < targetKeys.length; i++) {
const key = targetKeys[i];
this.style[key] = this.style[key];
}
this._transitionState(stateName, {
style: targetStyle
} as Props, animationCfg, this.getAnimationStyleProps() as MapToType<Props, boolean>);
}
else {
this.useStyle(targetStyle);
}
}
// Don't change z, z2 for element moved into hover layer.
// It's not necessary and will cause paint list order changed.
const statesKeys = this.__inHover ? PRIMARY_STATES_KEYS_IN_HOVER_LAYER : PRIMARY_STATES_KEYS;
for (let i = 0; i < statesKeys.length; i++) {
let key = statesKeys[i];
if (state && state[key] != null) {
// Replace if it exist in target state
(this as any)[key] = state[key];
}
else if (needsRestoreToNormal) {
// Restore to normal state
if (normalState[key] != null) {
(this as any)[key] = normalState[key];
}
}
}
}
protected _mergeStates(states: DisplayableState[]) {
const mergedState = super._mergeStates(states) as DisplayableState;
let mergedStyle: Props['style'];
for (let i = 0; i < states.length; i++) {
const state = states[i];
if (state.style) {
mergedStyle = mergedStyle || {};
this._mergeStyle(mergedStyle, state.style);
}
}
if (mergedStyle) {
mergedState.style = mergedStyle;
}
return mergedState;
}
protected _mergeStyle(
targetStyle: CommonStyleProps,
sourceStyle: CommonStyleProps
) {
extend(targetStyle, sourceStyle);
return targetStyle;
}
getAnimationStyleProps() {
return DEFAULT_COMMON_ANIMATION_PROPS;
}
/**
* The string value of `textPosition` needs to be calculated to a real postion.
* For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]`
* by default. See `contain/text.js#calculateTextPosition` for more details.
* But some coutom shapes like "pin", "flag" have center that is not exactly
* `[width/2, height/2]`. So we provide this hook to customize the calculation
* for those shapes. It will be called if the `style.textPosition` is a string.
* @param out Prepared out object. If not provided, this method should
* be responsible for creating one.
* @param style
* @param rect {x, y, width, height}
* @return out The same as the input out.
* {
* x: number. mandatory.
* y: number. mandatory.
* textAlign: string. optional. use style.textAlign by default.
* textVerticalAlign: string. optional. use style.textVerticalAlign by default.
* }
*/
// calculateTextPosition: (out: CalculateTextPositionResult, style: Dictionary<any>, rect: RectLike) => CalculateTextPositionResult
protected static initDefaultProps = (function () {
const dispProto = Displayable.prototype;
dispProto.type = 'displayable';
dispProto.invisible = false;
dispProto.z = 0;
dispProto.z2 = 0;
dispProto.zlevel = 0;
dispProto.culling = false;
dispProto.cursor = 'pointer';
dispProto.rectHover = false;
dispProto.incremental = false;
dispProto._rect = null;
dispProto.dirtyRectTolerance = 0;
dispProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT;
})()
}
const tmpRect = new BoundingRect(0, 0, 0, 0);
const viewRect = new BoundingRect(0, 0, 0, 0);
function isDisplayableCulled(el: Displayable, width: number, height: number) {
tmpRect.copy(el.getBoundingRect());
if (el.transform) {
tmpRect.applyTransform(el.transform);
}
viewRect.width = width;
viewRect.height = height;
return !tmpRect.intersect(viewRect);
}
export default Displayable;
+42
View File
@@ -0,0 +1,42 @@
// TODO Should GradientObject been LinearGradientObject | RadialGradientObject
export interface GradientObject {
id?: number
type: string
colorStops: GradientColorStop[]
global?: boolean
}
export interface InnerGradientObject extends GradientObject {
__canvasGradient: CanvasGradient
}
export interface GradientColorStop {
offset: number
color: string
}
export default class Gradient {
id?: number
type: string
colorStops: GradientColorStop[]
global: boolean
constructor(colorStops: GradientColorStop[]) {
this.colorStops = colorStops || [];
}
addColorStop(offset: number, color: string) {
this.colorStops.push({
offset,
color
});
}
}
+300
View File
@@ -0,0 +1,300 @@
/**
* Group是一个容器,可以插入子节点,Group的变换也会被应用到子节点上
* @module zrender/graphic/Group
* @example
* const Group = require('zrender/graphic/Group');
* const Circle = require('zrender/graphic/shape/Circle');
* const g = new Group();
* g.position[0] = 100;
* g.position[1] = 100;
* g.add(new Circle({
* style: {
* x: 100,
* y: 100,
* r: 20,
* }
* }));
* zr.add(g);
*/
import * as zrUtil from '../core/util';
import Element, { ElementProps } from '../Element';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Displayable from './Displayable';
import { ZRenderType } from '../zrender';
export interface GroupProps extends ElementProps {
}
class Group extends Element<GroupProps> {
readonly isGroup = true
private _children: Element[] = []
constructor(opts?: GroupProps) {
super();
this.attr(opts);
}
/**
* Get children reference.
*/
childrenRef() {
return this._children;
}
/**
* Get children copy.
*/
children() {
return this._children.slice();
}
/**
* 获取指定 index 的儿子节点
*/
childAt(idx: number): Element {
return this._children[idx];
}
/**
* 获取指定名字的儿子节点
*/
childOfName(name: string): Element {
const children = this._children;
for (let i = 0; i < children.length; i++) {
if (children[i].name === name) {
return children[i];
}
}
}
childCount(): number {
return this._children.length;
}
/**
* 添加子节点到最后
*/
add(child: Element): Group {
if (child) {
if (child !== this && child.parent !== this) {
this._children.push(child);
this._doAdd(child);
}
if (process.env.NODE_ENV !== 'production') {
if (child.__hostTarget) {
throw 'This elemenet has been used as an attachment';
}
}
}
return this;
}
/**
* 添加子节点在 nextSibling 之前
*/
addBefore(child: Element, nextSibling: Element) {
if (child && child !== this && child.parent !== this
&& nextSibling && nextSibling.parent === this) {
const children = this._children;
const idx = children.indexOf(nextSibling);
if (idx >= 0) {
children.splice(idx, 0, child);
this._doAdd(child);
}
}
return this;
}
replace(oldChild: Element, newChild: Element) {
const idx = zrUtil.indexOf(this._children, oldChild);
if (idx >= 0) {
this.replaceAt(newChild, idx);
}
return this;
}
replaceAt(child: Element, index: number) {
const children = this._children;
const old = children[index];
if (child && child !== this && child.parent !== this && child !== old) {
children[index] = child;
old.parent = null;
const zr = this.__zr;
if (zr) {
old.removeSelfFromZr(zr);
}
this._doAdd(child);
}
return this;
}
_doAdd(child: Element) {
if (child.parent) {
// Parent must be a group
(child.parent as Group).remove(child);
}
child.parent = this;
const zr = this.__zr;
if (zr && zr !== (child as Group).__zr) { // Only group has __storage
child.addSelfToZr(zr);
}
zr && zr.refresh();
}
/**
* Remove child
* @param child
*/
remove(child: Element) {
const zr = this.__zr;
const children = this._children;
const idx = zrUtil.indexOf(children, child);
if (idx < 0) {
return this;
}
children.splice(idx, 1);
child.parent = null;
if (zr) {
child.removeSelfFromZr(zr);
}
zr && zr.refresh();
return this;
}
/**
* Remove all children
*/
removeAll() {
const children = this._children;
const zr = this.__zr;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (zr) {
child.removeSelfFromZr(zr);
}
child.parent = null;
}
children.length = 0;
return this;
}
/**
* 遍历所有子节点
*/
eachChild<Context>(
cb: (this: Context, el: Element, index?: number) => void,
context?: Context
) {
const children = this._children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
cb.call(context, child, i);
}
return this;
}
/**
* Visit all descendants.
* Return false in callback to stop visit descendants of current node
*/
// TODO Group itself should also invoke the callback.
traverse<T>(
cb: (this: T, el: Element) => boolean | void,
context?: T
) {
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
const stopped = cb.call(context, child);
if (child.isGroup && !stopped) {
child.traverse(cb, context);
}
}
return this;
}
addSelfToZr(zr: ZRenderType) {
super.addSelfToZr(zr);
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
child.addSelfToZr(zr);
}
}
removeSelfFromZr(zr: ZRenderType) {
super.removeSelfFromZr(zr);
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
child.removeSelfFromZr(zr);
}
}
getBoundingRect(includeChildren?: Element[]): BoundingRect {
// TODO Caching
const tmpRect = new BoundingRect(0, 0, 0, 0);
const children = includeChildren || this._children;
const tmpMat: MatrixArray = [];
let rect = null;
for (let i = 0; i < children.length; i++) {
const child = children[i];
// TODO invisible?
if (child.ignore || (child as Displayable).invisible) {
continue;
}
const childRect = child.getBoundingRect();
const transform = child.getLocalTransform(tmpMat);
// TODO
// The boundingRect cacluated by transforming original
// rect may be bigger than the actual bundingRect when rotation
// is used. (Consider a circle rotated aginst its center, where
// the actual boundingRect should be the same as that not be
// rotated.) But we can not find better approach to calculate
// actual boundingRect yet, considering performance.
if (transform) {
BoundingRect.applyTransform(tmpRect, childRect, transform);
rect = rect || tmpRect.clone();
rect.union(tmpRect);
}
else {
rect = rect || childRect.clone();
rect.union(childRect);
}
}
return rect || tmpRect;
}
}
Group.prototype.type = 'group';
// Storage will use childrenRef to get children to render.
export interface GroupLike extends Element {
childrenRef(): Element[]
}
export default Group;
+126
View File
@@ -0,0 +1,126 @@
import Displayable, { DisplayableProps,
CommonStyleProps,
DEFAULT_COMMON_STYLE,
DisplayableStatePropNames,
DEFAULT_COMMON_ANIMATION_PROPS
} from './Displayable';
import BoundingRect from '../core/BoundingRect';
import { ImageLike, MapToType } from '../core/types';
import { defaults, createObject } from '../core/util';
import { ElementCommonState } from '../Element';
export interface ImageStyleProps extends CommonStyleProps {
image?: string | ImageLike
x?: number
y?: number
width?: number
height?: number
sx?: number
sy?: number
sWidth?: number
sHeight?: number
}
export const DEFAULT_IMAGE_STYLE: CommonStyleProps = defaults({
x: 0,
y: 0
}, DEFAULT_COMMON_STYLE);
export const DEFAULT_IMAGE_ANIMATION_PROPS: MapToType<ImageProps, boolean> = {
style: defaults<MapToType<ImageStyleProps, boolean>, MapToType<ImageStyleProps, boolean>>({
x: true,
y: true,
width: true,
height: true,
sx: true,
sy: true,
sWidth: true,
sHeight: true
}, DEFAULT_COMMON_ANIMATION_PROPS.style)
};
export interface ImageProps extends DisplayableProps {
style?: ImageStyleProps
onload?: (image: ImageLike) => void
}
export type ImageState = Pick<ImageProps, DisplayableStatePropNames> & ElementCommonState
function isImageLike(source: unknown): source is HTMLImageElement {
return !!(source
&& typeof source !== 'string'
// Image source is an image, canvas, video.
&& (source as HTMLImageElement).width && (source as HTMLImageElement).height);
}
class ZRImage extends Displayable<ImageProps> {
style: ImageStyleProps
// FOR CANVAS RENDERER
__image: ImageLike
// FOR SVG RENDERER
__imageSrc: string
onload: (image: ImageLike) => void
/**
* Create an image style object with default values in it's prototype.
* @override
*/
createStyle(obj?: ImageStyleProps) {
return createObject(DEFAULT_IMAGE_STYLE, obj);
}
private _getSize(dim: 'width' | 'height') {
const style = this.style;
let size = style[dim];
if (size != null) {
return size;
}
const imageSource = isImageLike(style.image)
? style.image : this.__image;
if (!imageSource) {
return 0;
}
const otherDim = dim === 'width' ? 'height' : 'width';
let otherDimSize = style[otherDim];
if (otherDimSize == null) {
return imageSource[dim];
}
else {
return imageSource[dim] / imageSource[otherDim] * otherDimSize;
}
}
getWidth(): number {
return this._getSize('width');
}
getHeight(): number {
return this._getSize('height');
}
getAnimationStyleProps() {
return DEFAULT_IMAGE_ANIMATION_PROPS;
}
getBoundingRect(): BoundingRect {
const style = this.style;
if (!this._rect) {
this._rect = new BoundingRect(
style.x || 0, style.y || 0, this.getWidth(), this.getHeight()
);
}
return this._rect;
}
}
ZRImage.prototype.type = 'image';
export default ZRImage;
+148
View File
@@ -0,0 +1,148 @@
/**
* Displayable for incremental rendering. It will be rendered in a separate layer
* IncrementalDisplay have two main methods. `clearDisplayables` and `addDisplayables`
* addDisplayables will render the added displayables incremetally.
*
* It use a notClear flag to tell the painter don't clear the layer if it's the first element.
*
* It's not available for SVG rendering.
*/
import Displayble from './Displayable';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Group from './Group';
const m: MatrixArray = [];
// TODO Style override ?
export default class IncrementalDisplayable extends Displayble {
notClear: boolean = true
incremental = true
private _displayables: Displayble[] = []
private _temporaryDisplayables: Displayble[] = []
private _cursor = 0
traverse<T>(
cb: (this: T, el: this) => void,
context: T
) {
cb.call(context, this);
}
useStyle() {
// Use an empty style
// PENDING
this.style = {};
}
// getCurrentCursor / updateCursorAfterBrush
// is used in graphic.ts. It's not provided for developers
getCursor() {
return this._cursor;
}
// Update cursor after brush.
innerAfterBrush() {
this._cursor = this._displayables.length;
}
clearDisplaybles() {
this._displayables = [];
this._temporaryDisplayables = [];
this._cursor = 0;
this.markRedraw();
this.notClear = false;
}
clearTemporalDisplayables() {
this._temporaryDisplayables = [];
}
addDisplayable(displayable: Displayble, notPersistent?: boolean) {
if (notPersistent) {
this._temporaryDisplayables.push(displayable);
}
else {
this._displayables.push(displayable);
}
this.markRedraw();
}
addDisplayables(displayables: Displayble[], notPersistent?: boolean) {
notPersistent = notPersistent || false;
for (let i = 0; i < displayables.length; i++) {
this.addDisplayable(displayables[i], notPersistent);
}
}
getDisplayables(): Displayble[] {
return this._displayables;
}
getTemporalDisplayables(): Displayble[] {
return this._temporaryDisplayables;
}
eachPendingDisplayable(cb: (displayable: Displayble) => void) {
for (let i = this._cursor; i < this._displayables.length; i++) {
cb && cb(this._displayables[i]);
}
for (let i = 0; i < this._temporaryDisplayables.length; i++) {
cb && cb(this._temporaryDisplayables[i]);
}
}
update() {
this.updateTransform();
for (let i = this._cursor; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
// PENDING
displayable.parent = this as unknown as Group;
displayable.update();
displayable.parent = null;
}
for (let i = 0; i < this._temporaryDisplayables.length; i++) {
const displayable = this._temporaryDisplayables[i];
// PENDING
displayable.parent = this as unknown as Group;
displayable.update();
displayable.parent = null;
}
}
getBoundingRect() {
if (!this._rect) {
const rect = new BoundingRect(Infinity, Infinity, -Infinity, -Infinity);
for (let i = 0; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
const childRect = displayable.getBoundingRect().clone();
if (displayable.needLocalTransform()) {
childRect.applyTransform(displayable.getLocalTransform(m));
}
rect.union(childRect);
}
this._rect = rect;
}
return this._rect;
}
contain(x: number, y: number): boolean {
const localPos = this.transformCoordToLocal(x, y);
const rect = this.getBoundingRect();
if (rect.contain(localPos[0], localPos[1])) {
for (let i = 0; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
if (displayable.contain(x, y)) {
return true;
}
}
}
return false;
}
}
+49
View File
@@ -0,0 +1,49 @@
import Gradient, {GradientObject, GradientColorStop} from './Gradient';
export interface LinearGradientObject extends GradientObject {
type: 'linear'
x: number
y: number
x2: number
y2: number
}
/**
* x, y, x2, y2 are all percent from 0 to 1 when globalCoord is false
*/
export default class LinearGradient extends Gradient {
type: 'linear'
x: number
y: number
x2: number
y2: number
constructor(
x: number, y: number, x2: number, y2: number,
colorStops?: GradientColorStop[], globalCoord?: boolean
) {
super(colorStops);
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {type: 'linear', colorStops: ...}`, where
// this constructor will not be called.
this.x = x == null ? 0 : x;
this.y = y == null ? 0 : y;
this.x2 = x2 == null ? 1 : x2;
this.y2 = y2 == null ? 0 : y2;
// Can be cloned
this.type = 'linear';
// If use global coord
this.global = globalCoord || false;
}
};
+677
View File
@@ -0,0 +1,677 @@
import Displayable, { DisplayableProps,
CommonStyleProps,
DEFAULT_COMMON_STYLE,
DisplayableStatePropNames,
DEFAULT_COMMON_ANIMATION_PROPS
} from './Displayable';
import Element, { ElementAnimateConfig } from '../Element';
import PathProxy from '../core/PathProxy';
import * as pathContain from '../contain/path';
import { PatternObject } from './Pattern';
import { Dictionary, PropType, MapToType } from '../core/types';
import BoundingRect from '../core/BoundingRect';
import { LinearGradientObject } from './LinearGradient';
import { RadialGradientObject } from './RadialGradient';
import { defaults, keys, extend, clone, isString, createObject } from '../core/util';
import Animator from '../animation/Animator';
import { lum } from '../tool/color';
import { DARK_LABEL_COLOR, LIGHT_LABEL_COLOR, DARK_MODE_THRESHOLD, LIGHTER_LABEL_COLOR } from '../config';
import { REDRAW_BIT, SHAPE_CHANGED_BIT, STYLE_CHANGED_BIT } from './constants';
import { TRANSFORMABLE_PROPS } from '../core/Transformable';
export interface PathStyleProps extends CommonStyleProps {
fill?: string | PatternObject | LinearGradientObject | RadialGradientObject
stroke?: string | PatternObject | LinearGradientObject | RadialGradientObject
decal?: PatternObject
/**
* Still experimental, not works weel on arc with edge cases(large angle).
*/
strokePercent?: number
strokeNoScale?: boolean
fillOpacity?: number
strokeOpacity?: number
/**
* `true` is not supported.
* `false`/`null`/`undefined` are the same.
* `false` is used to remove lineDash in some
* case that `null`/`undefined` can not be set.
* (e.g., emphasis.lineStyle in echarts)
*/
lineDash?: false | number[] | 'solid' | 'dashed' | 'dotted'
lineDashOffset?: number
lineWidth?: number
lineCap?: CanvasLineCap
lineJoin?: CanvasLineJoin
miterLimit?: number
/**
* Paint order, if do stroke first. Similar to SVG paint-order
* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order
*/
strokeFirst?: boolean
}
export const DEFAULT_PATH_STYLE: PathStyleProps = defaults({
fill: '#000',
stroke: null,
strokePercent: 1,
fillOpacity: 1,
strokeOpacity: 1,
lineDashOffset: 0,
lineWidth: 1,
lineCap: 'butt',
miterLimit: 10,
strokeNoScale: false,
strokeFirst: false
} as PathStyleProps, DEFAULT_COMMON_STYLE);
export const DEFAULT_PATH_ANIMATION_PROPS: MapToType<PathProps, boolean> = {
style: defaults<MapToType<PathStyleProps, boolean>, MapToType<PathStyleProps, boolean>>({
fill: true,
stroke: true,
strokePercent: true,
fillOpacity: true,
strokeOpacity: true,
lineDashOffset: true,
lineWidth: true,
miterLimit: true
} as MapToType<PathStyleProps, boolean>, DEFAULT_COMMON_ANIMATION_PROPS.style)
};
export interface PathProps extends DisplayableProps {
strokeContainThreshold?: number
segmentIgnoreThreshold?: number
subPixelOptimize?: boolean
style?: PathStyleProps
shape?: Dictionary<any>
autoBatch?: boolean
__value?: (string | number)[] | (string | number)
buildPath?: (
ctx: PathProxy | CanvasRenderingContext2D,
shapeCfg: Dictionary<any>,
inBatch?: boolean
) => void
}
type PathKey = keyof PathProps
type PathPropertyType = PropType<PathProps, PathKey>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Path<Props extends PathProps = PathProps> {
animate(key?: '', loop?: boolean): Animator<this>
animate(key: 'style', loop?: boolean): Animator<this['style']>
animate(key: 'shape', loop?: boolean): Animator<this['shape']>
getState(stateName: string): PathState
ensureState(stateName: string): PathState
states: Dictionary<PathState>
stateProxy: (stateName: string) => PathState
}
export type PathStatePropNames = DisplayableStatePropNames | 'shape';
export type PathState = Pick<PathProps, PathStatePropNames> & {
hoverLayer?: boolean
}
const pathCopyParams = (TRANSFORMABLE_PROPS as readonly string[]).concat(['invisible',
'culling', 'z', 'z2', 'zlevel', 'parent'
]) as (keyof Path)[];
class Path<Props extends PathProps = PathProps> extends Displayable<Props> {
path: PathProxy
strokeContainThreshold: number
// This item default to be false. But in map series in echarts,
// in order to improve performance, it should be set to true,
// so the shorty segment won't draw.
segmentIgnoreThreshold: number
subPixelOptimize: boolean
style: PathStyleProps
/**
* If element can be batched automatically
*/
autoBatch: boolean
private _rectStroke: BoundingRect
protected _normalState: PathState
protected _decalEl: Path
// Must have an initial value on shape.
// It will be assigned by default value.
shape: Dictionary<any>
constructor(opts?: Props) {
super(opts);
}
update() {
super.update();
const style = this.style;
if (style.decal) {
const decalEl: Path = this._decalEl = this._decalEl || new Path();
if (decalEl.buildPath === Path.prototype.buildPath) {
decalEl.buildPath = ctx => {
this.buildPath(ctx, this.shape);
};
}
decalEl.silent = true;
const decalElStyle = decalEl.style;
for (let key in style) {
if ((decalElStyle as any)[key] !== (style as any)[key]) {
(decalElStyle as any)[key] = (style as any)[key];
}
}
decalElStyle.fill = style.fill ? style.decal : null;
decalElStyle.decal = null;
decalElStyle.shadowColor = null;
style.strokeFirst && (decalElStyle.stroke = null);
for (let i = 0; i < pathCopyParams.length; ++i) {
(decalEl as any)[pathCopyParams[i]] = this[pathCopyParams[i]];
}
decalEl.__dirty |= REDRAW_BIT;
}
else if (this._decalEl) {
this._decalEl = null;
}
}
getDecalElement() {
return this._decalEl;
}
protected _init(props?: Props) {
// Init default properties
const keysArr = keys(props);
this.shape = this.getDefaultShape();
const defaultStyle = this.getDefaultStyle();
if (defaultStyle) {
this.useStyle(defaultStyle);
}
for (let i = 0; i < keysArr.length; i++) {
const key = keysArr[i];
const value = props[key];
if (key === 'style') {
if (!this.style) {
// PENDING Reuse style object if possible?
this.useStyle(value as Props['style']);
}
else {
extend(this.style, value as Props['style']);
}
}
else if (key === 'shape') {
// this.shape = value;
extend(this.shape, value as Props['shape']);
}
else {
super.attrKV(key as any, value);
}
}
// Create an empty one if no style object exists.
if (!this.style) {
this.useStyle({});
}
// const defaultShape = this.getDefaultShape();
// if (!this.shape) {
// this.shape = defaultShape;
// }
// else {
// defaults(this.shape, defaultShape);
// }
}
protected getDefaultStyle(): Props['style'] {
return null;
}
// Needs to override
protected getDefaultShape() {
return {};
}
protected canBeInsideText() {
return this.hasFill();
}
protected getInsideTextFill() {
const pathFill = this.style.fill;
if (pathFill !== 'none') {
if (isString(pathFill)) {
const fillLum = lum(pathFill, 0);
// Determin text color based on the lum of path fill.
// TODO use (1 - DARK_MODE_THRESHOLD)?
if (fillLum > 0.5) { // TODO Consider background lum?
return DARK_LABEL_COLOR;
}
else if (fillLum > 0.2) {
return LIGHTER_LABEL_COLOR;
}
return LIGHT_LABEL_COLOR;
}
else if (pathFill) {
return LIGHT_LABEL_COLOR;
}
}
return DARK_LABEL_COLOR;
}
protected getInsideTextStroke(textFill?: string) {
const pathFill = this.style.fill;
// Not stroke on none fill object or gradient object
if (isString(pathFill)) {
const zr = this.__zr;
const isDarkMode = !!(zr && zr.isDarkMode());
const isDarkLabel = lum(textFill, 0) < DARK_MODE_THRESHOLD;
// All dark or all light.
if (isDarkMode === isDarkLabel) {
return pathFill;
}
}
}
// When bundling path, some shape may decide if use moveTo to begin a new subpath or closePath
// Like in circle
buildPath(
ctx: PathProxy | CanvasRenderingContext2D,
shapeCfg: Dictionary<any>,
inBatch?: boolean
) {}
pathUpdated() {
this.__dirty &= ~SHAPE_CHANGED_BIT;
}
getUpdatedPathProxy(inBatch?: boolean) {
// Update path proxy data to latest.
!this.path && this.createPathProxy();
this.path.beginPath();
this.buildPath(this.path, this.shape, inBatch);
return this.path;
}
createPathProxy() {
this.path = new PathProxy(false);
}
hasStroke() {
const style = this.style;
const stroke = style.stroke;
return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0));
}
hasFill() {
const style = this.style;
const fill = style.fill;
return fill != null && fill !== 'none';
}
getBoundingRect(): BoundingRect {
let rect = this._rect;
const style = this.style;
const needsUpdateRect = !rect;
if (needsUpdateRect) {
let firstInvoke = false;
if (!this.path) {
firstInvoke = true;
// Create path on demand.
this.createPathProxy();
}
let path = this.path;
if (firstInvoke || (this.__dirty & SHAPE_CHANGED_BIT)) {
path.beginPath();
this.buildPath(path, this.shape, false);
this.pathUpdated();
}
rect = path.getBoundingRect();
}
this._rect = rect;
if (this.hasStroke() && this.path && this.path.len() > 0) {
// Needs update rect with stroke lineWidth when
// 1. Element changes scale or lineWidth
// 2. Shape is changed
const rectStroke = this._rectStroke || (this._rectStroke = rect.clone());
if (this.__dirty || needsUpdateRect) {
rectStroke.copy(rect);
// PENDING, Min line width is needed when line is horizontal or vertical
const lineScale = style.strokeNoScale ? this.getLineScale() : 1;
// FIXME Must after updateTransform
let w = style.lineWidth;
// Only add extra hover lineWidth when there are no fill
if (!this.hasFill()) {
const strokeContainThreshold = this.strokeContainThreshold;
w = Math.max(w, strokeContainThreshold == null ? 4 : strokeContainThreshold);
}
// Consider line width
// Line scale can't be 0;
if (lineScale > 1e-10) {
rectStroke.width += w / lineScale;
rectStroke.height += w / lineScale;
rectStroke.x -= w / lineScale / 2;
rectStroke.y -= w / lineScale / 2;
}
}
// Return rect with stroke
return rectStroke;
}
return rect;
}
contain(x: number, y: number): boolean {
const localPos = this.transformCoordToLocal(x, y);
const rect = this.getBoundingRect();
const style = this.style;
x = localPos[0];
y = localPos[1];
if (rect.contain(x, y)) {
const pathProxy = this.path;
if (this.hasStroke()) {
let lineWidth = style.lineWidth;
let lineScale = style.strokeNoScale ? this.getLineScale() : 1;
// Line scale can't be 0;
if (lineScale > 1e-10) {
// Only add extra hover lineWidth when there are no fill
if (!this.hasFill()) {
lineWidth = Math.max(lineWidth, this.strokeContainThreshold);
}
if (pathContain.containStroke(
pathProxy, lineWidth / lineScale, x, y
)) {
return true;
}
}
}
if (this.hasFill()) {
return pathContain.contain(pathProxy, x, y);
}
}
return false;
}
/**
* Shape changed
*/
dirtyShape() {
this.__dirty |= SHAPE_CHANGED_BIT;
if (this._rect) {
this._rect = null;
}
if (this._decalEl) {
this._decalEl.dirtyShape();
}
this.markRedraw();
}
dirty() {
this.dirtyStyle();
this.dirtyShape();
}
/**
* Alias for animate('shape')
* @param {boolean} loop
*/
animateShape(loop: boolean) {
return this.animate('shape', loop);
}
// Override updateDuringAnimation
updateDuringAnimation(targetKey: string) {
if (targetKey === 'style') {
this.dirtyStyle();
}
else if (targetKey === 'shape') {
this.dirtyShape();
}
else {
this.markRedraw();
}
}
// Overwrite attrKV
attrKV(key: PathKey, value: PathPropertyType) {
// FIXME
if (key === 'shape') {
this.setShape(value as Props['shape']);
}
else {
super.attrKV(key as keyof DisplayableProps, value);
}
}
setShape(obj: Props['shape']): this
setShape<T extends keyof Props['shape']>(obj: T, value: Props['shape'][T]): this
setShape(keyOrObj: keyof Props['shape'] | Props['shape'], value?: unknown): this {
let shape = this.shape;
if (!shape) {
shape = this.shape = {};
}
// Path from string may not have shape
if (typeof keyOrObj === 'string') {
shape[keyOrObj] = value;
}
else {
extend(shape, keyOrObj as Props['shape']);
}
this.dirtyShape();
return this;
}
/**
* If shape changed. used with dirtyShape
*/
shapeChanged() {
return !!(this.__dirty & SHAPE_CHANGED_BIT);
}
/**
* Create a path style object with default values in it's prototype.
* @override
*/
createStyle(obj?: Props['style']) {
return createObject(DEFAULT_PATH_STYLE, obj);
}
protected _innerSaveToNormal(toState: PathState) {
super._innerSaveToNormal(toState);
const normalState = this._normalState;
// Clone a new one. DON'T share object reference between states and current using.
// TODO: Clone array in shape?.
// TODO: Only save changed shape.
if (toState.shape && !normalState.shape) {
normalState.shape = extend({}, this.shape);
}
}
protected _applyStateObj(
stateName: string,
state: PathState,
normalState: PathState,
keepCurrentStates: boolean,
transition: boolean,
animationCfg: ElementAnimateConfig
) {
super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
const needsRestoreToNormal = !(state && keepCurrentStates);
let targetShape: Props['shape'];
if (state && state.shape) {
// Only animate changed properties.
if (transition) {
if (keepCurrentStates) {
targetShape = state.shape;
}
else {
// Inherits from normal state.
targetShape = extend({}, normalState.shape);
extend(targetShape, state.shape);
}
}
else {
// Because the shape will be replaced. So inherits from current shape.
targetShape = extend({}, keepCurrentStates ? this.shape : normalState.shape);
extend(targetShape, state.shape);
}
}
else if (needsRestoreToNormal) {
targetShape = normalState.shape;
}
if (targetShape) {
if (transition) {
// Clone a new shape.
this.shape = extend({}, this.shape);
// Only supports transition on primary props. Because shape is not deep cloned.
const targetShapePrimaryProps: Props['shape'] = {};
const shapeKeys = keys(targetShape);
for (let i = 0; i < shapeKeys.length; i++) {
const key = shapeKeys[i];
if (typeof targetShape[key] === 'object') {
(this.shape as Props['shape'])[key] = targetShape[key];
}
else {
targetShapePrimaryProps[key] = targetShape[key];
}
}
this._transitionState(stateName, {
shape: targetShapePrimaryProps
} as Props, animationCfg);
}
else {
this.shape = targetShape;
this.dirtyShape();
}
}
}
protected _mergeStates(states: PathState[]) {
const mergedState = super._mergeStates(states) as PathState;
let mergedShape: Props['shape'];
for (let i = 0; i < states.length; i++) {
const state = states[i];
if (state.shape) {
mergedShape = mergedShape || {};
this._mergeStyle(mergedShape, state.shape);
}
}
if (mergedShape) {
mergedState.shape = mergedShape;
}
return mergedState;
}
getAnimationStyleProps() {
return DEFAULT_PATH_ANIMATION_PROPS;
}
/**
* If path shape is zero area
*/
isZeroArea(): boolean {
return false;
}
/**
* 扩展一个 Path element, 比如星形,圆等。
* Extend a path element
* @DEPRECATED Use class extends
* @param props
* @param props.type Path type
* @param props.init Initialize
* @param props.buildPath Overwrite buildPath method
* @param props.style Extended default style config
* @param props.shape Extended default shape config
*/
static extend<Shape extends Dictionary<any>>(defaultProps: {
type?: string
shape?: Shape
style?: PathStyleProps
beforeBrush?: Displayable['beforeBrush']
afterBrush?: Displayable['afterBrush']
getBoundingRect?: Displayable['getBoundingRect']
calculateTextPosition?: Element['calculateTextPosition']
buildPath(this: Path, ctx: CanvasRenderingContext2D | PathProxy, shape: Shape, inBatch?: boolean): void
init?(this: Path, opts: PathProps): void // TODO Should be SubPathOption
}): {
new(opts?: PathProps & {shape: Shape}): Path
} {
interface SubPathOption extends PathProps {
shape: Shape
}
class Sub extends Path {
shape: Shape
getDefaultStyle() {
return clone(defaultProps.style);
}
getDefaultShape() {
return clone(defaultProps.shape);
}
constructor(opts?: SubPathOption) {
super(opts);
defaultProps.init && defaultProps.init.call(this as any, opts);
}
}
// TODO Legacy usage. Extend functions
for (let key in defaultProps) {
if (typeof (defaultProps as any)[key] === 'function') {
(Sub.prototype as any)[key] = (defaultProps as any)[key];
}
}
// Sub.prototype.buildPath = defaultProps.buildPath;
// Sub.prototype.beforeBrush = defaultProps.beforeBrush;
// Sub.prototype.afterBrush = defaultProps.afterBrush;
return Sub as any;
}
protected static initDefaultProps = (function () {
const pathProto = Path.prototype;
pathProto.type = 'path';
pathProto.strokeContainThreshold = 5;
pathProto.segmentIgnoreThreshold = 0;
pathProto.subPixelOptimize = false;
pathProto.autoBatch = false;
pathProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT | SHAPE_CHANGED_BIT;
})()
}
export default Path;
+83
View File
@@ -0,0 +1,83 @@
import { ImageLike } from '../core/types';
import { SVGVNode } from '../svg/core';
type ImagePatternRepeat = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'
export interface PatternObjectBase {
id?: number
// type is now unused, so make it optional
type?: 'pattern'
x?: number
y?: number
rotation?: number
scaleX?: number
scaleY?: number
}
export interface ImagePatternObject extends PatternObjectBase {
image: ImageLike | string
repeat?: ImagePatternRepeat
/**
* Width and height of image.
* `imageWidth` and `imageHeight` are only used in svg-ssr renderer.
* Because we can't get the size of image in svg-ssr renderer.
* They need to be give explictly.
*/
imageWidth?: number
imageHeight?: number
}
export interface InnerImagePatternObject extends ImagePatternObject {
// Cached image. Which is created in the canvas painter.
__image?: ImageLike
}
export interface SVGPatternObject extends PatternObjectBase {
/**
* svg vnode can only be used in svg renderer currently.
* svgWidth, svgHeight defines width and height used for pattern.
*/
svgElement?: SVGVNode
svgWidth?: number
svgHeight?: number
}
export type PatternObject = ImagePatternObject | SVGPatternObject
class Pattern {
type: 'pattern'
image: ImageLike | string
/**
* svg element can only be used in svg renderer currently.
*
* Will be string if using SSR rendering.
*/
svgElement: SVGElement | string
repeat: ImagePatternRepeat
x: number
y: number
rotation: number
scaleX: number
scaleY: number
constructor(image: ImageLike | string, repeat: ImagePatternRepeat) {
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {image: ...}`, where this constructor will not be called.
this.image = image;
this.repeat = repeat;
this.x = 0;
this.y = 0;
this.rotation = 0;
this.scaleX = 1;
this.scaleY = 1;
}
}
export default Pattern;
+43
View File
@@ -0,0 +1,43 @@
import Gradient, {GradientColorStop, GradientObject} from './Gradient';
export interface RadialGradientObject extends GradientObject {
type: 'radial'
x: number
y: number
r: number
}
/**
* x, y, r are all percent from 0 to 1 when globalCoord is false
*/
class RadialGradient extends Gradient {
type: 'radial'
x: number
y: number
r: number
constructor(
x: number, y: number, r: number,
colorStops?: GradientColorStop[], globalCoord?: boolean
) {
super(colorStops);
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {type: 'radial', colorStops: ...}`, where
// this constructor will not be called.
this.x = x == null ? 0.5 : x;
this.y = y == null ? 0.5 : y;
this.r = r == null ? 0.5 : r;
// Can be cloned
this.type = 'radial';
// If use global coord
this.global = globalCoord || false;
}
}
export default RadialGradient;
+123
View File
@@ -0,0 +1,123 @@
import Displayable, { DisplayableProps, DisplayableStatePropNames } from './Displayable';
import { getBoundingRect } from '../contain/text';
import BoundingRect from '../core/BoundingRect';
import { PathStyleProps, DEFAULT_PATH_STYLE } from './Path';
import { createObject, defaults } from '../core/util';
import { FontStyle, FontWeight, TextAlign, TextVerticalAlign } from '../core/types';
import { DEFAULT_FONT } from '../core/platform';
export interface TSpanStyleProps extends PathStyleProps {
x?: number
y?: number
// TODO Text is assigned inside zrender
text?: string
// Final generated font string
// Used in canvas, and when developers specified it.
font?: string
// Value for each part of font
// Used in svg.
// NOTE: font should always been sync with these 4 properties.
fontSize?: number
fontWeight?: FontWeight
fontStyle?: FontStyle
fontFamily?: string
textAlign?: CanvasTextAlign
textBaseline?: CanvasTextBaseline
}
export const DEFAULT_TSPAN_STYLE: TSpanStyleProps = defaults({
strokeFirst: true,
font: DEFAULT_FONT,
x: 0,
y: 0,
textAlign: 'left',
textBaseline: 'top',
miterLimit: 2
} as TSpanStyleProps, DEFAULT_PATH_STYLE);
export interface TSpanProps extends DisplayableProps {
style?: TSpanStyleProps
}
export type TSpanState = Pick<TSpanProps, DisplayableStatePropNames>
class TSpan extends Displayable<TSpanProps> {
style: TSpanStyleProps
hasStroke() {
const style = this.style;
const stroke = style.stroke;
return stroke != null && stroke !== 'none' && style.lineWidth > 0;
}
hasFill() {
const style = this.style;
const fill = style.fill;
return fill != null && fill !== 'none';
}
/**
* Create an image style object with default values in it's prototype.
* @override
*/
createStyle(obj?: TSpanStyleProps) {
return createObject(DEFAULT_TSPAN_STYLE, obj);
}
/**
* Set bounding rect calculated from Text
* For reducing time of calculating bounding rect.
*/
setBoundingRect(rect: BoundingRect) {
this._rect = rect;
}
getBoundingRect(): BoundingRect {
const style = this.style;
if (!this._rect) {
let text = style.text;
text != null ? (text += '') : (text = '');
const rect = getBoundingRect(
text,
style.font,
style.textAlign as TextAlign,
style.textBaseline as TextVerticalAlign
);
rect.x += style.x || 0;
rect.y += style.y || 0;
if (this.hasStroke()) {
const w = style.lineWidth;
rect.x -= w / 2;
rect.y -= w / 2;
rect.width += w;
rect.height += w;
}
this._rect = rect;
}
return this._rect;
}
protected static initDefaultProps = (function () {
const tspanProto = TSpan.prototype;
// TODO Calculate tolerance smarter
tspanProto.dirtyRectTolerance = 10;
})()
}
TSpan.prototype.type = 'tspan';
export default TSpan;
+1039
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
// Bit masks to check which parts of element needs to be updated.
export const REDRAW_BIT = 1;
export const STYLE_CHANGED_BIT = 2;
export const SHAPE_CHANGED_BIT = 4;
+106
View File
@@ -0,0 +1,106 @@
import LRU from '../../core/LRU';
import { platformApi } from '../../core/platform';
import { ImageLike } from '../../core/types';
const globalImageCache = new LRU<CachedImageObj>(50);
type PendingWrap = {
hostEl: {dirty: () => void}
cb: (image: ImageLike, payload: any) => void
cbPayload: any
}
type CachedImageObj = {
image: ImageLike
pending: PendingWrap[]
}
export function findExistImage(newImageOrSrc: string | ImageLike): ImageLike {
if (typeof newImageOrSrc === 'string') {
const cachedImgObj = globalImageCache.get(newImageOrSrc);
return cachedImgObj && cachedImgObj.image;
}
else {
return newImageOrSrc;
}
}
/**
* Caution: User should cache loaded images, but not just count on LRU.
* Consider if required images more than LRU size, will dead loop occur?
*
* @param newImageOrSrc
* @param image Existent image.
* @param hostEl For calling `dirty`.
* @param onload params: (image, cbPayload)
* @param cbPayload Payload on cb calling.
* @return image
*/
export function createOrUpdateImage<T>(
newImageOrSrc: string | ImageLike,
image: ImageLike,
hostEl: { dirty: () => void },
onload?: (image: ImageLike, payload: T) => void,
cbPayload?: T
) {
if (!newImageOrSrc) {
return image;
}
else if (typeof newImageOrSrc === 'string') {
// Image should not be loaded repeatly.
if ((image && (image as any).__zrImageSrc === newImageOrSrc) || !hostEl) {
return image;
}
// Only when there is no existent image or existent image src
// is different, this method is responsible for load.
const cachedImgObj = globalImageCache.get(newImageOrSrc);
const pendingWrap = {hostEl: hostEl, cb: onload, cbPayload: cbPayload};
if (cachedImgObj) {
image = cachedImgObj.image;
!isImageReady(image) && cachedImgObj.pending.push(pendingWrap);
}
else {
image = platformApi.loadImage(
newImageOrSrc, imageOnLoad, imageOnLoad
);
(image as any).__zrImageSrc = newImageOrSrc;
globalImageCache.put(
newImageOrSrc,
(image as any).__cachedImgObj = {
image: image,
pending: [pendingWrap]
}
);
}
return image;
}
// newImageOrSrc is an HTMLImageElement or HTMLCanvasElement or Canvas
else {
return newImageOrSrc;
}
}
function imageOnLoad(this: any) {
const cachedImgObj = this.__cachedImgObj;
this.onload = this.onerror = this.__cachedImgObj = null;
for (let i = 0; i < cachedImgObj.pending.length; i++) {
const pendingWrap = cachedImgObj.pending[i];
const cb = pendingWrap.cb;
cb && cb(this, pendingWrap.cbPayload);
pendingWrap.hostEl.dirty();
}
cachedImgObj.pending.length = 0;
}
export function isImageReady(image: ImageLike) {
return image && image.width && image.height;
}
+756
View File
@@ -0,0 +1,756 @@
import * as imageHelper from '../helper/image';
import {
extend,
retrieve2,
retrieve3,
reduce
} from '../../core/util';
import { TextAlign, TextVerticalAlign, ImageLike, Dictionary } from '../../core/types';
import { TextStyleProps } from '../Text';
import { getLineHeight, getWidth, parsePercent } from '../../contain/text';
const STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g;
interface InnerTruncateOption {
maxIteration?: number
// If truncate result are less than minChar, ellipsis will not show
// which is better for user hint in some cases
minChar?: number
// When all truncated, use the placeholder
placeholder?: string
maxIterations?: number
}
interface InnerPreparedTruncateOption extends Required<InnerTruncateOption> {
font: string
ellipsis: string
ellipsisWidth: number
contentWidth: number
containerWidth: number
cnCharWidth: number
ascCharWidth: number
}
/**
* Show ellipsis if overflow.
*/
export function truncateText(
text: string,
containerWidth: number,
font: string,
ellipsis: string,
options: InnerTruncateOption
): string {
if (!containerWidth) {
return '';
}
const textLines = (text + '').split('\n');
options = prepareTruncateOptions(containerWidth, font, ellipsis, options);
// FIXME
// It is not appropriate that every line has '...' when truncate multiple lines.
for (let i = 0, len = textLines.length; i < len; i++) {
textLines[i] = truncateSingleLine(textLines[i], options as InnerPreparedTruncateOption);
}
return textLines.join('\n');
}
function prepareTruncateOptions(
containerWidth: number,
font: string,
ellipsis: string,
options: InnerTruncateOption
): InnerPreparedTruncateOption {
options = options || {};
let preparedOpts = extend({}, options) as InnerPreparedTruncateOption;
preparedOpts.font = font;
ellipsis = retrieve2(ellipsis, '...');
preparedOpts.maxIterations = retrieve2(options.maxIterations, 2);
const minChar = preparedOpts.minChar = retrieve2(options.minChar, 0);
// FIXME
// Other languages?
preparedOpts.cnCharWidth = getWidth('国', font);
// FIXME
// Consider proportional font?
const ascCharWidth = preparedOpts.ascCharWidth = getWidth('a', font);
preparedOpts.placeholder = retrieve2(options.placeholder, '');
// Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'.
// Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'.
let contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap.
for (let i = 0; i < minChar && contentWidth >= ascCharWidth; i++) {
contentWidth -= ascCharWidth;
}
let ellipsisWidth = getWidth(ellipsis, font);
if (ellipsisWidth > contentWidth) {
ellipsis = '';
ellipsisWidth = 0;
}
contentWidth = containerWidth - ellipsisWidth;
preparedOpts.ellipsis = ellipsis;
preparedOpts.ellipsisWidth = ellipsisWidth;
preparedOpts.contentWidth = contentWidth;
preparedOpts.containerWidth = containerWidth;
return preparedOpts;
}
function truncateSingleLine(textLine: string, options: InnerPreparedTruncateOption): string {
const containerWidth = options.containerWidth;
const font = options.font;
const contentWidth = options.contentWidth;
if (!containerWidth) {
return '';
}
let lineWidth = getWidth(textLine, font);
if (lineWidth <= containerWidth) {
return textLine;
}
for (let j = 0; ; j++) {
if (lineWidth <= contentWidth || j >= options.maxIterations) {
textLine += options.ellipsis;
break;
}
const subLength = j === 0
? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth)
: lineWidth > 0
? Math.floor(textLine.length * contentWidth / lineWidth)
: 0;
textLine = textLine.substr(0, subLength);
lineWidth = getWidth(textLine, font);
}
if (textLine === '') {
textLine = options.placeholder;
}
return textLine;
}
function estimateLength(
text: string, contentWidth: number, ascCharWidth: number, cnCharWidth: number
): number {
let width = 0;
let i = 0;
for (let len = text.length; i < len && width < contentWidth; i++) {
const charCode = text.charCodeAt(i);
width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth;
}
return i;
}
export interface PlainTextContentBlock {
lineHeight: number
// Line height of actual content.
calculatedLineHeight: number
contentWidth: number
contentHeight: number
width: number
height: number
/**
* Real text width containing padding.
* It should be the same as `width` if background is rendered
* and `width` is set by user.
*/
outerWidth: number
outerHeight: number
lines: string[]
}
export function parsePlainText(
text: string,
style?: TextStyleProps
): PlainTextContentBlock {
text != null && (text += '');
// textPadding has been normalized
const overflow = style.overflow;
const padding = style.padding as number[];
const font = style.font;
const truncate = overflow === 'truncate';
const calculatedLineHeight = getLineHeight(font);
const lineHeight = retrieve2(style.lineHeight, calculatedLineHeight);
const bgColorDrawn = !!(style.backgroundColor);
const truncateLineOverflow = style.lineOverflow === 'truncate';
let width = style.width;
let lines: string[];
if (width != null && (overflow === 'break' || overflow === 'breakAll')) {
lines = text ? wrapText(text, style.font, width, overflow === 'breakAll', 0).lines : [];
}
else {
lines = text ? text.split('\n') : [];
}
const contentHeight = lines.length * lineHeight;
const height = retrieve2(style.height, contentHeight);
// Truncate lines.
if (contentHeight > height && truncateLineOverflow) {
const lineCount = Math.floor(height / lineHeight);
lines = lines.slice(0, lineCount);
// TODO If show ellipse for line truncate
// if (style.ellipsis) {
// const options = prepareTruncateOptions(width, font, style.ellipsis, {
// minChar: style.truncateMinChar,
// placeholder: style.placeholder
// });
// lines[lineCount - 1] = truncateSingleLine(lastLine, options);
// }
}
if (text && truncate && width != null) {
const options = prepareTruncateOptions(width, font, style.ellipsis, {
minChar: style.truncateMinChar,
placeholder: style.placeholder
});
// Having every line has '...' when truncate multiple lines.
for (let i = 0; i < lines.length; i++) {
lines[i] = truncateSingleLine(lines[i], options);
}
}
// Calculate real text width and height
let outerHeight = height;
let contentWidth = 0;
for (let i = 0; i < lines.length; i++) {
contentWidth = Math.max(getWidth(lines[i], font), contentWidth);
}
if (width == null) {
// When width is not explicitly set, use outerWidth as width.
width = contentWidth;
}
let outerWidth = contentWidth;
if (padding) {
outerHeight += padding[0] + padding[2];
outerWidth += padding[1] + padding[3];
width += padding[1] + padding[3];
}
if (bgColorDrawn) {
// When render background, outerWidth should be the same as width.
outerWidth = width;
}
return {
lines: lines,
height: height,
outerWidth: outerWidth,
outerHeight: outerHeight,
lineHeight: lineHeight,
calculatedLineHeight: calculatedLineHeight,
contentWidth: contentWidth,
contentHeight: contentHeight,
width: width
};
}
class RichTextToken {
styleName: string
text: string
width: number
height: number
// Inner height exclude padding
innerHeight: number
// Width and height of actual text content.
contentHeight: number
contentWidth: number
lineHeight: number
font: string
align: TextAlign
verticalAlign: TextVerticalAlign
textPadding: number[]
percentWidth?: string
isLineHolder: boolean
}
class RichTextLine {
lineHeight: number
width: number
tokens: RichTextToken[] = []
constructor(tokens?: RichTextToken[]) {
if (tokens) {
this.tokens = tokens;
}
}
}
export class RichTextContentBlock {
// width/height of content
width: number = 0
height: number = 0
// Calculated text height
contentWidth: number = 0
contentHeight: number = 0
// outerWidth/outerHeight with padding
outerWidth: number = 0
outerHeight: number = 0
lines: RichTextLine[] = []
}
type WrapInfo = {
width: number,
accumWidth: number,
breakAll: boolean
}
/**
* For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx'
* Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'.
* If styleName is undefined, it is plain text.
*/
export function parseRichText(text: string, style: TextStyleProps) {
const contentBlock = new RichTextContentBlock();
text != null && (text += '');
if (!text) {
return contentBlock;
}
const topWidth = style.width;
const topHeight = style.height;
const overflow = style.overflow;
let wrapInfo: WrapInfo = (overflow === 'break' || overflow === 'breakAll') && topWidth != null
? {width: topWidth, accumWidth: 0, breakAll: overflow === 'breakAll'}
: null;
let lastIndex = STYLE_REG.lastIndex = 0;
let result;
while ((result = STYLE_REG.exec(text)) != null) {
const matchedIndex = result.index;
if (matchedIndex > lastIndex) {
pushTokens(contentBlock, text.substring(lastIndex, matchedIndex), style, wrapInfo);
}
pushTokens(contentBlock, result[2], style, wrapInfo, result[1]);
lastIndex = STYLE_REG.lastIndex;
}
if (lastIndex < text.length) {
pushTokens(contentBlock, text.substring(lastIndex, text.length), style, wrapInfo);
}
// For `textWidth: xx%`
let pendingList = [];
let calculatedHeight = 0;
let calculatedWidth = 0;
const stlPadding = style.padding as number[];
const truncate = overflow === 'truncate';
const truncateLine = style.lineOverflow === 'truncate';
// let prevToken: RichTextToken;
function finishLine(line: RichTextLine, lineWidth: number, lineHeight: number) {
line.width = lineWidth;
line.lineHeight = lineHeight;
calculatedHeight += lineHeight;
calculatedWidth = Math.max(calculatedWidth, lineWidth);
}
// Calculate layout info of tokens.
outer: for (let i = 0; i < contentBlock.lines.length; i++) {
const line = contentBlock.lines[i];
let lineHeight = 0;
let lineWidth = 0;
for (let j = 0; j < line.tokens.length; j++) {
const token = line.tokens[j];
const tokenStyle = token.styleName && style.rich[token.styleName] || {};
// textPadding should not inherit from style.
const textPadding = token.textPadding = tokenStyle.padding as number[];
const paddingH = textPadding ? textPadding[1] + textPadding[3] : 0;
const font = token.font = tokenStyle.font || style.font;
token.contentHeight = getLineHeight(font);
// textHeight can be used when textVerticalAlign is specified in token.
let tokenHeight = retrieve2(
// textHeight should not be inherited, consider it can be specified
// as box height of the block.
tokenStyle.height, token.contentHeight
);
token.innerHeight = tokenHeight;
textPadding && (tokenHeight += textPadding[0] + textPadding[2]);
token.height = tokenHeight;
// Inlcude padding in lineHeight.
token.lineHeight = retrieve3(
tokenStyle.lineHeight, style.lineHeight, tokenHeight
);
token.align = tokenStyle && tokenStyle.align || style.align;
token.verticalAlign = tokenStyle && tokenStyle.verticalAlign || 'middle';
if (truncateLine && topHeight != null && calculatedHeight + token.lineHeight > topHeight) {
// TODO Add ellipsis on the previous token.
// prevToken.text =
if (j > 0) {
line.tokens = line.tokens.slice(0, j);
finishLine(line, lineWidth, lineHeight);
contentBlock.lines = contentBlock.lines.slice(0, i + 1);
}
else {
contentBlock.lines = contentBlock.lines.slice(0, i);
}
break outer;
}
let styleTokenWidth = tokenStyle.width;
let tokenWidthNotSpecified = styleTokenWidth == null || styleTokenWidth === 'auto';
// Percent width, can be `100%`, can be used in drawing separate
// line when box width is needed to be auto.
if (typeof styleTokenWidth === 'string' && styleTokenWidth.charAt(styleTokenWidth.length - 1) === '%') {
token.percentWidth = styleTokenWidth;
pendingList.push(token);
token.contentWidth = getWidth(token.text, font);
// Do not truncate in this case, because there is no user case
// and it is too complicated.
}
else {
if (tokenWidthNotSpecified) {
// FIXME: If image is not loaded and textWidth is not specified, calling
// `getBoundingRect()` will not get correct result.
const textBackgroundColor = tokenStyle.backgroundColor;
let bgImg = textBackgroundColor && (textBackgroundColor as { image: ImageLike }).image;
if (bgImg) {
bgImg = imageHelper.findExistImage(bgImg);
if (imageHelper.isImageReady(bgImg)) {
// Update token width from image size.
token.width = Math.max(token.width, bgImg.width * tokenHeight / bgImg.height);
}
}
}
const remainTruncWidth = truncate && topWidth != null
? topWidth - lineWidth : null;
if (remainTruncWidth != null && remainTruncWidth < token.width) {
if (!tokenWidthNotSpecified || remainTruncWidth < paddingH) {
token.text = '';
token.width = token.contentWidth = 0;
}
else {
token.text = truncateText(
token.text, remainTruncWidth - paddingH, font, style.ellipsis,
{minChar: style.truncateMinChar}
);
token.width = token.contentWidth = getWidth(token.text, font);
}
}
else {
token.contentWidth = getWidth(token.text, font);
}
}
token.width += paddingH;
lineWidth += token.width;
tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight));
// prevToken = token;
}
finishLine(line, lineWidth, lineHeight);
}
contentBlock.outerWidth = contentBlock.width = retrieve2(topWidth, calculatedWidth);
contentBlock.outerHeight = contentBlock.height = retrieve2(topHeight, calculatedHeight);
contentBlock.contentHeight = calculatedHeight;
contentBlock.contentWidth = calculatedWidth;
if (stlPadding) {
contentBlock.outerWidth += stlPadding[1] + stlPadding[3];
contentBlock.outerHeight += stlPadding[0] + stlPadding[2];
}
for (let i = 0; i < pendingList.length; i++) {
const token = pendingList[i];
const percentWidth = token.percentWidth;
// Should not base on outerWidth, because token can not be placed out of padding.
token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width;
}
return contentBlock;
}
type TokenStyle = TextStyleProps['rich'][string];
function pushTokens(
block: RichTextContentBlock,
str: string,
style: TextStyleProps,
wrapInfo: WrapInfo,
styleName?: string
) {
const isEmptyStr = str === '';
const tokenStyle: TokenStyle = styleName && style.rich[styleName] || {};
const lines = block.lines;
const font = tokenStyle.font || style.font;
let newLine = false;
let strLines;
let linesWidths;
if (wrapInfo) {
const tokenPadding = tokenStyle.padding as number[];
let tokenPaddingH = tokenPadding ? tokenPadding[1] + tokenPadding[3] : 0;
if (tokenStyle.width != null && tokenStyle.width !== 'auto') {
// Wrap the whole token if tokenWidth if fixed.
const outerWidth = parsePercent(tokenStyle.width, wrapInfo.width) + tokenPaddingH;
if (lines.length > 0) { // Not first line
if (outerWidth + wrapInfo.accumWidth > wrapInfo.width) {
// TODO Support wrap text in token.
strLines = str.split('\n');
newLine = true;
}
}
wrapInfo.accumWidth = outerWidth;
}
else {
const res = wrapText(str, font, wrapInfo.width, wrapInfo.breakAll, wrapInfo.accumWidth);
wrapInfo.accumWidth = res.accumWidth + tokenPaddingH;
linesWidths = res.linesWidths;
strLines = res.lines;
}
}
else {
strLines = str.split('\n');
}
for (let i = 0; i < strLines.length; i++) {
const text = strLines[i];
const token = new RichTextToken();
token.styleName = styleName;
token.text = text;
token.isLineHolder = !text && !isEmptyStr;
if (typeof tokenStyle.width === 'number') {
token.width = tokenStyle.width;
}
else {
token.width = linesWidths
? linesWidths[i] // Caculated width in the wrap
: getWidth(text, font);
}
// The first token should be appended to the last line if not new line.
if (!i && !newLine) {
const tokens = (lines[lines.length - 1] || (lines[0] = new RichTextLine())).tokens;
// Consider cases:
// (1) ''.split('\n') => ['', '\n', ''], the '' at the first item
// (which is a placeholder) should be replaced by new token.
// (2) A image backage, where token likes {a|}.
// (3) A redundant '' will affect textAlign in line.
// (4) tokens with the same tplName should not be merged, because
// they should be displayed in different box (with border and padding).
const tokensLen = tokens.length;
(tokensLen === 1 && tokens[0].isLineHolder)
? (tokens[0] = token)
// Consider text is '', only insert when it is the "lineHolder" or
// "emptyStr". Otherwise a redundant '' will affect textAlign in line.
: ((text || !tokensLen || isEmptyStr) && tokens.push(token));
}
// Other tokens always start a new line.
else {
// If there is '', insert it as a placeholder.
lines.push(new RichTextLine([token]));
}
}
}
function isLatin(ch: string) {
let code = ch.charCodeAt(0);
return code >= 0x21 && code <= 0x17F;
}
const breakCharMap = reduce(',&?/;] '.split(''), function (obj, ch) {
obj[ch] = true;
return obj;
}, {} as Dictionary<boolean>);
/**
* If break by word. For latin languages.
*/
function isWordBreakChar(ch: string) {
if (isLatin(ch)) {
if (breakCharMap[ch]) {
return true;
}
return false;
}
return true;
}
function wrapText(
text: string,
font: string,
lineWidth: number,
isBreakAll: boolean,
lastAccumWidth: number
) {
let lines: string[] = [];
let linesWidths: number[] = [];
let line = '';
let currentWord = '';
let currentWordWidth = 0;
let accumWidth = 0;
for (let i = 0; i < text.length; i++) {
const ch = text.charAt(i);
if (ch === '\n') {
if (currentWord) {
line += currentWord;
accumWidth += currentWordWidth;
}
lines.push(line);
linesWidths.push(accumWidth);
// Reset
line = '';
currentWord = '';
currentWordWidth = 0;
accumWidth = 0;
continue;
}
const chWidth = getWidth(ch, font);
const inWord = isBreakAll ? false : !isWordBreakChar(ch);
if (!lines.length
? lastAccumWidth + accumWidth + chWidth > lineWidth
: accumWidth + chWidth > lineWidth
) {
if (!accumWidth) { // If nothing appended yet.
if (inWord) {
// The word length is still too long for one line
// Force break the word
lines.push(currentWord);
linesWidths.push(currentWordWidth);
currentWord = ch;
currentWordWidth = chWidth;
}
else {
// lineWidth is too small for ch
lines.push(ch);
linesWidths.push(chWidth);
}
}
else if (line || currentWord) {
if (inWord) {
if (!line) {
// The one word is still too long for one line
// Force break the word
// TODO Keep the word?
line = currentWord;
currentWord = '';
currentWordWidth = 0;
accumWidth = currentWordWidth;
}
lines.push(line);
linesWidths.push(accumWidth - currentWordWidth);
// Break the whole word
currentWord += ch;
currentWordWidth += chWidth;
line = '';
accumWidth = currentWordWidth;
}
else {
// Append lastWord if have
if (currentWord) {
line += currentWord;
currentWord = '';
currentWordWidth = 0;
}
lines.push(line);
linesWidths.push(accumWidth);
line = ch;
accumWidth = chWidth;
}
}
continue;
}
accumWidth += chWidth;
if (inWord) {
currentWord += ch;
currentWordWidth += chWidth;
}
else {
// Append whole word
if (currentWord) {
line += currentWord;
// Reset
currentWord = '';
currentWordWidth = 0;
}
// Append character
line += ch;
}
}
if (!lines.length && !line) {
line = text;
currentWord = '';
currentWordWidth = 0;
}
// Append last line.
if (currentWord) {
line += currentWord;
}
if (line) {
lines.push(line);
linesWidths.push(accumWidth);
}
if (lines.length === 1) {
// No new line.
accumWidth += lastAccumWidth;
}
return {
// Accum width of last line
accumWidth,
lines: lines,
linesWidths
};
}
+43
View File
@@ -0,0 +1,43 @@
import smoothBezier from './smoothBezier';
import { VectorArray } from '../../core/vector';
import PathProxy from '../../core/PathProxy';
export function buildPath(
ctx: CanvasRenderingContext2D | PathProxy,
shape: {
points: VectorArray[],
smooth?: number
smoothConstraint?: VectorArray[]
},
closePath: boolean
) {
const smooth = shape.smooth;
let points = shape.points;
if (points && points.length >= 2) {
if (smooth) {
const controlPoints = smoothBezier(
points, smooth, closePath, shape.smoothConstraint
);
ctx.moveTo(points[0][0], points[0][1]);
const len = points.length;
for (let i = 0; i < (closePath ? len : len - 1); i++) {
const cp1 = controlPoints[i * 2];
const cp2 = controlPoints[i * 2 + 1];
const p = points[(i + 1) % len];
ctx.bezierCurveTo(
cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]
);
}
}
else {
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1, l = points.length; i < l; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
}
closePath && ctx.closePath();
}
}
+87
View File
@@ -0,0 +1,87 @@
import PathProxy from '../../core/PathProxy';
export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
x: number
y: number
width: number
height: number
r?: number | number[]
}) {
let x = shape.x;
let y = shape.y;
let width = shape.width;
let height = shape.height;
let r = shape.r;
let r1;
let r2;
let r3;
let r4;
// Convert width and height to positive for better borderRadius
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}
if (typeof r === 'number') {
r1 = r2 = r3 = r4 = r;
}
else if (r instanceof Array) {
if (r.length === 1) {
r1 = r2 = r3 = r4 = r[0];
}
else if (r.length === 2) {
r1 = r3 = r[0];
r2 = r4 = r[1];
}
else if (r.length === 3) {
r1 = r[0];
r2 = r4 = r[1];
r3 = r[2];
}
else {
r1 = r[0];
r2 = r[1];
r3 = r[2];
r4 = r[3];
}
}
else {
r1 = r2 = r3 = r4 = 0;
}
let total;
if (r1 + r2 > width) {
total = r1 + r2;
r1 *= width / total;
r2 *= width / total;
}
if (r3 + r4 > width) {
total = r3 + r4;
r3 *= width / total;
r4 *= width / total;
}
if (r2 + r3 > height) {
total = r2 + r3;
r2 *= height / total;
r3 *= height / total;
}
if (r1 + r4 > height) {
total = r1 + r4;
r1 *= height / total;
r4 *= height / total;
}
ctx.moveTo(x + r1, y);
ctx.lineTo(x + width - r2, y);
r2 !== 0 && ctx.arc(x + width - r2, y + r2, r2, -Math.PI / 2, 0);
ctx.lineTo(x + width, y + height - r3);
r3 !== 0 && ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI / 2);
ctx.lineTo(x + r4, y + height);
r4 !== 0 && ctx.arc(x + r4, y + height - r4, r4, Math.PI / 2, Math.PI);
ctx.lineTo(x, y + r1);
r1 !== 0 && ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5);
}
+321
View File
@@ -0,0 +1,321 @@
import PathProxy from '../../core/PathProxy';
import { isArray } from '../../core/util';
const PI = Math.PI;
const PI2 = PI * 2;
const mathSin = Math.sin;
const mathCos = Math.cos;
const mathACos = Math.acos;
const mathATan2 = Math.atan2;
const mathAbs = Math.abs;
const mathSqrt = Math.sqrt;
const mathMax = Math.max;
const mathMin = Math.min;
const e = 1e-4;
function intersect(
x0: number, y0: number,
x1: number, y1: number,
x2: number, y2: number,
x3: number, y3: number
): [number, number] {
const dx10 = x1 - x0;
const dy10 = y1 - y0;
const dx32 = x3 - x2;
const dy32 = y3 - y2;
let t = dy32 * dx10 - dx32 * dy10;
if (t * t < e) {
return;
}
t = (dx32 * (y0 - y2) - dy32 * (x0 - x2)) / t;
return [x0 + t * dx10, y0 + t * dy10];
}
// Compute perpendicular offset line of length rc.
function computeCornerTangents(
x0: number, y0: number,
x1: number, y1: number,
radius: number, cr: number,
clockwise: boolean
) {
const x01 = x0 - x1;
const y01 = y0 - y1;
const lo = (clockwise ? cr : -cr) / mathSqrt(x01 * x01 + y01 * y01);
const ox = lo * y01;
const oy = -lo * x01;
const x11 = x0 + ox;
const y11 = y0 + oy;
const x10 = x1 + ox;
const y10 = y1 + oy;
const x00 = (x11 + x10) / 2;
const y00 = (y11 + y10) / 2;
const dx = x10 - x11;
const dy = y10 - y11;
const d2 = dx * dx + dy * dy;
const r = radius - cr;
const s = x11 * y10 - x10 * y11;
const d = (dy < 0 ? -1 : 1) * mathSqrt(mathMax(0, r * r * d2 - s * s));
let cx0 = (s * dy - dx * d) / d2;
let cy0 = (-s * dx - dy * d) / d2;
const cx1 = (s * dy + dx * d) / d2;
const cy1 = (-s * dx + dy * d) / d2;
const dx0 = cx0 - x00;
const dy0 = cy0 - y00;
const dx1 = cx1 - x00;
const dy1 = cy1 - y00;
// Pick the closer of the two intersection points
// TODO: Is there a faster way to determine which intersection to use?
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) {
cx0 = cx1;
cy0 = cy1;
}
return {
cx: cx0,
cy: cy0,
x0: -ox,
y0: -oy,
x1: cx0 * (radius / r - 1),
y1: cy0 * (radius / r - 1)
};
}
// For compatibility, don't use normalizeCssArray
// 5 represents [5, 5, 5, 5]
// [5] represents [5, 5, 0, 0]
// [5, 10] represents [5, 5, 10, 10]
// [5, 10, 15] represents [5, 10, 15, 15]
// [5, 10, 15, 20] represents [5, 10, 15, 20]
function normalizeCornerRadius(cr: number | number[]): number[] {
let arr: number[];
if (isArray(cr)) {
const len = cr.length;
if (!len) {
return cr as number[];
}
if (len === 1) {
arr = [cr[0], cr[0], 0, 0];
}
else if (len === 2) {
arr = [cr[0], cr[0], cr[1], cr[1]];
}
else if (len === 3) {
arr = cr.concat(cr[2]);
}
else {
arr = cr;
}
}
else {
arr = [cr, cr, cr, cr];
}
return arr;
}
export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
cx: number
cy: number
startAngle: number
endAngle: number
clockwise?: boolean,
r?: number,
r0?: number,
cornerRadius?: number | number[]
}) {
let radius = mathMax(shape.r, 0);
let innerRadius = mathMax(shape.r0 || 0, 0);
const hasRadius = radius > 0;
const hasInnerRadius = innerRadius > 0;
if (!hasRadius && !hasInnerRadius) {
return;
}
if (!hasRadius) {
// use innerRadius as radius if no radius
radius = innerRadius;
innerRadius = 0;
}
if (innerRadius > radius) {
// swap, ensure that radius is always larger than innerRadius
const tmp = radius;
radius = innerRadius;
innerRadius = tmp;
}
const { startAngle, endAngle } = shape;
if (isNaN(startAngle) || isNaN(endAngle)) {
return;
}
const { cx, cy } = shape;
const clockwise = !!shape.clockwise;
let arc = mathAbs(endAngle - startAngle);
const mod = arc > PI2 && arc % PI2;
mod > e && (arc = mod);
// is a point
if (!(radius > e)) {
ctx.moveTo(cx, cy);
}
// is a circle or annulus
else if (arc > PI2 - e) {
ctx.moveTo(
cx + radius * mathCos(startAngle),
cy + radius * mathSin(startAngle)
);
ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
if (innerRadius > e) {
ctx.moveTo(
cx + innerRadius * mathCos(endAngle),
cy + innerRadius * mathSin(endAngle)
);
ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
}
}
// is a circular or annular sector
else {
let icrStart;
let icrEnd;
let ocrStart;
let ocrEnd;
let ocrs;
let ocre;
let icrs;
let icre;
let ocrMax;
let icrMax;
let limitedOcrMax;
let limitedIcrMax;
let xre;
let yre;
let xirs;
let yirs;
const xrs = radius * mathCos(startAngle);
const yrs = radius * mathSin(startAngle);
const xire = innerRadius * mathCos(endAngle);
const yire = innerRadius * mathSin(endAngle);
const hasArc = arc > e;
if (hasArc) {
const cornerRadius = shape.cornerRadius;
if (cornerRadius) {
[icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius);
}
const halfRd = mathAbs(radius - innerRadius) / 2;
ocrs = mathMin(halfRd, ocrStart);
ocre = mathMin(halfRd, ocrEnd);
icrs = mathMin(halfRd, icrStart);
icre = mathMin(halfRd, icrEnd);
limitedOcrMax = ocrMax = mathMax(ocrs, ocre);
limitedIcrMax = icrMax = mathMax(icrs, icre);
// draw corner radius
if (ocrMax > e || icrMax > e) {
xre = radius * mathCos(endAngle);
yre = radius * mathSin(endAngle);
xirs = innerRadius * mathCos(startAngle);
yirs = innerRadius * mathSin(startAngle);
// restrict the max value of corner radius
if (arc < PI) {
const it = intersect(xrs, yrs, xirs, yirs, xre, yre, xire, yire);
if (it) {
const x0 = xrs - it[0];
const y0 = yrs - it[1];
const x1 = xre - it[0];
const y1 = yre - it[1];
const a = 1 / mathSin(
// eslint-disable-next-line max-len
mathACos((x0 * x1 + y0 * y1) / (mathSqrt(x0 * x0 + y0 * y0) * mathSqrt(x1 * x1 + y1 * y1))) / 2
);
const b = mathSqrt(it[0] * it[0] + it[1] * it[1]);
limitedOcrMax = mathMin(ocrMax, (radius - b) / (a + 1));
limitedIcrMax = mathMin(icrMax, (innerRadius - b) / (a - 1));
}
}
}
}
// the sector is collapsed to a line
if (!hasArc) {
ctx.moveTo(cx + xrs, cy + yrs);
}
// the outer ring has corners
else if (limitedOcrMax > e) {
const crStart = mathMin(ocrStart, limitedOcrMax);
const crEnd = mathMin(ocrEnd, limitedOcrMax);
const ct0 = computeCornerTangents(xirs, yirs, xrs, yrs, radius, crStart, clockwise);
const ct1 = computeCornerTangents(xre, yre, xire, yire, radius, crEnd, clockwise);
ctx.moveTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
// Have the corners merged?
if (limitedOcrMax < ocrMax && crStart === crEnd) {
// eslint-disable-next-line max-len
ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedOcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
else {
// draw the two corners and the ring
// eslint-disable-next-line max-len
crStart > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crStart, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
// eslint-disable-next-line max-len
ctx.arc(cx, cy, radius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), !clockwise);
// eslint-disable-next-line max-len
crEnd > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crEnd, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
}
// the outer ring is a circular arc
else {
ctx.moveTo(cx + xrs, cy + yrs);
ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
}
// no inner ring, is a circular sector
if (!(innerRadius > e) || !hasArc) {
ctx.lineTo(cx + xire, cy + yire);
}
// the inner ring has corners
else if (limitedIcrMax > e) {
const crStart = mathMin(icrStart, limitedIcrMax);
const crEnd = mathMin(icrEnd, limitedIcrMax);
const ct0 = computeCornerTangents(xire, yire, xre, yre, innerRadius, -crEnd, clockwise);
const ct1 = computeCornerTangents(xrs, yrs, xirs, yirs, innerRadius, -crStart, clockwise);
ctx.lineTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
// Have the corners merged?
if (limitedIcrMax < icrMax && crStart === crEnd) {
// eslint-disable-next-line max-len
ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedIcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
// draw the two corners and the ring
else {
// eslint-disable-next-line max-len
crEnd > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crEnd, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
// eslint-disable-next-line max-len
ctx.arc(cx, cy, innerRadius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), clockwise);
// eslint-disable-next-line max-len
crStart > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crStart, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
}
// the inner ring is just a circular arc
else {
// FIXME: if no lineTo, svg renderer will perform an abnormal drawing behavior.
ctx.lineTo(cx + xire, cy + yire);
ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
}
}
ctx.closePath();
}
+104
View File
@@ -0,0 +1,104 @@
/**
* 贝塞尔平滑曲线
*/
import {
min as v2Min,
max as v2Max,
scale as v2Scale,
distance as v2Distance,
add as v2Add,
clone as v2Clone,
sub as v2Sub,
VectorArray
} from '../../core/vector';
/**
* 贝塞尔平滑曲线
* @param points 线段顶点数组
* @param smooth 平滑等级, 0-1
* @param isLoop
* @param constraint 将计算出来的控制点约束在一个包围盒内
* 比如 [[0, 0], [100, 100]], 这个包围盒会与
* 整个折线的包围盒做一个并集用来约束控制点。
* @param 计算出来的控制点数组
*/
export default function smoothBezier(
points: VectorArray[],
smooth?: number,
isLoop?: boolean,
constraint?: VectorArray[]
) {
const cps = [];
const v: VectorArray = [];
const v1: VectorArray = [];
const v2: VectorArray = [];
let prevPoint;
let nextPoint;
let min;
let max;
if (constraint) {
min = [Infinity, Infinity];
max = [-Infinity, -Infinity];
for (let i = 0, len = points.length; i < len; i++) {
v2Min(min, min, points[i]);
v2Max(max, max, points[i]);
}
// 与指定的包围盒做并集
v2Min(min, min, constraint[0]);
v2Max(max, max, constraint[1]);
}
for (let i = 0, len = points.length; i < len; i++) {
const point = points[i];
if (isLoop) {
prevPoint = points[i ? i - 1 : len - 1];
nextPoint = points[(i + 1) % len];
}
else {
if (i === 0 || i === len - 1) {
cps.push(v2Clone(points[i]));
continue;
}
else {
prevPoint = points[i - 1];
nextPoint = points[i + 1];
}
}
v2Sub(v, nextPoint, prevPoint);
// use degree to scale the handle length
v2Scale(v, v, smooth);
let d0 = v2Distance(point, prevPoint);
let d1 = v2Distance(point, nextPoint);
const sum = d0 + d1;
if (sum !== 0) {
d0 /= sum;
d1 /= sum;
}
v2Scale(v1, v, -d0);
v2Scale(v2, v, d1);
const cp0 = v2Add([], point, v1);
const cp1 = v2Add([], point, v2);
if (constraint) {
v2Max(cp0, cp0, min);
v2Min(cp0, cp0, max);
v2Max(cp1, cp1, min);
v2Min(cp1, cp1, max);
}
cps.push(cp0);
cps.push(cp1);
}
if (isLoop) {
cps.push(cps.shift());
}
return cps;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Catmull-Rom spline 插值折线
*/
import {distance as v2Distance, VectorArray} from '../../core/vector';
function interpolate(
p0: number, p1: number, p2: number, p3: number, t: number, t2: number, t3: number
) {
const v0 = (p2 - p0) * 0.5;
const v1 = (p3 - p1) * 0.5;
return (2 * (p1 - p2) + v0 + v1) * t3
+ (-3 * (p1 - p2) - 2 * v0 - v1) * t2
+ v0 * t + p1;
}
export default function smoothSpline(points: VectorArray[], isLoop?: boolean): VectorArray[] {
const len = points.length;
const ret = [];
let distance = 0;
for (let i = 1; i < len; i++) {
distance += v2Distance(points[i - 1], points[i]);
}
let segs = distance / 2;
segs = segs < len ? len : segs;
for (let i = 0; i < segs; i++) {
const pos = i / (segs - 1) * (isLoop ? len : len - 1);
const idx = Math.floor(pos);
const w = pos - idx;
let p0;
let p1 = points[idx % len];
let p2;
let p3;
if (!isLoop) {
p0 = points[idx === 0 ? idx : idx - 1];
p2 = points[idx > len - 2 ? len - 1 : idx + 1];
p3 = points[idx > len - 3 ? len - 1 : idx + 2];
}
else {
p0 = points[(idx - 1 + len) % len];
p2 = points[(idx + 1) % len];
p3 = points[(idx + 2) % len];
}
const w2 = w * w;
const w3 = w * w2;
ret.push([
interpolate(p0[0], p1[0], p2[0], p3[0], w, w2, w3),
interpolate(p0[1], p1[1], p2[1], p3[1], w, w2, w3)
]);
}
return ret;
}
+134
View File
@@ -0,0 +1,134 @@
import { PathStyleProps } from '../Path';
/**
* Sub-pixel optimize for canvas rendering, prevent from blur
* when rendering a thin vertical/horizontal line.
*/
const round = Math.round;
type LineShape = {
x1: number
y1: number
x2: number
y2: number
}
type RectShape = {
x: number
y: number
width: number
height: number
r?: number | number[]
}
/**
* Sub pixel optimize line for canvas
*
* @param outputShape The modification will be performed on `outputShape`.
* `outputShape` and `inputShape` can be the same object.
* `outputShape` object can be used repeatly, because all of
* the `x1`, `x2`, `y1`, `y2` will be assigned in this method.
*/
export function subPixelOptimizeLine(
outputShape: Partial<LineShape>,
inputShape: LineShape,
style: Pick<PathStyleProps, 'lineWidth'> // DO not optimize when lineWidth is 0
): LineShape {
if (!inputShape) {
return;
}
const x1 = inputShape.x1;
const x2 = inputShape.x2;
const y1 = inputShape.y1;
const y2 = inputShape.y2;
outputShape.x1 = x1;
outputShape.x2 = x2;
outputShape.y1 = y1;
outputShape.y2 = y2;
const lineWidth = style && style.lineWidth;
if (!lineWidth) {
return outputShape as LineShape;
}
if (round(x1 * 2) === round(x2 * 2)) {
outputShape.x1 = outputShape.x2 = subPixelOptimize(x1, lineWidth, true);
}
if (round(y1 * 2) === round(y2 * 2)) {
outputShape.y1 = outputShape.y2 = subPixelOptimize(y1, lineWidth, true);
}
return outputShape as LineShape;
}
/**
* Sub pixel optimize rect for canvas
*
* @param outputShape The modification will be performed on `outputShape`.
* `outputShape` and `inputShape` can be the same object.
* `outputShape` object can be used repeatly, because all of
* the `x`, `y`, `width`, `height` will be assigned in this method.
*/
export function subPixelOptimizeRect(
outputShape: Partial<RectShape>,
inputShape: RectShape,
style: Pick<PathStyleProps, 'lineWidth'> // DO not optimize when lineWidth is 0
): RectShape {
if (!inputShape) {
return;
}
const originX = inputShape.x;
const originY = inputShape.y;
const originWidth = inputShape.width;
const originHeight = inputShape.height;
outputShape.x = originX;
outputShape.y = originY;
outputShape.width = originWidth;
outputShape.height = originHeight;
const lineWidth = style && style.lineWidth;
if (!lineWidth) {
return outputShape as RectShape;
}
outputShape.x = subPixelOptimize(originX, lineWidth, true);
outputShape.y = subPixelOptimize(originY, lineWidth, true);
outputShape.width = Math.max(
subPixelOptimize(originX + originWidth, lineWidth, false) - outputShape.x,
originWidth === 0 ? 0 : 1
);
outputShape.height = Math.max(
subPixelOptimize(originY + originHeight, lineWidth, false) - outputShape.y,
originHeight === 0 ? 0 : 1
);
return outputShape as RectShape;
}
/**
* Sub pixel optimize for canvas
*
* @param position Coordinate, such as x, y
* @param lineWidth If `null`/`undefined`/`0`, do not optimize.
* @param positiveOrNegative Default false (negative).
* @return Optimized position.
*/
export function subPixelOptimize(
position: number,
lineWidth?: number,
positiveOrNegative?: boolean
) {
if (!lineWidth) {
return position;
}
// Assure that (position + lineWidth / 2) is near integer edge,
// otherwise line will be fuzzy in canvas.
const doubledPosition = round(position * 2);
return (doubledPosition + round(lineWidth)) % 2 === 0
? doubledPosition / 2
: (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* 圆弧
*/
import Path, { PathProps } from '../Path';
export class ArcShape {
cx = 0;
cy = 0;
r = 0;
startAngle = 0;
endAngle = Math.PI * 2
clockwise? = true
}
export interface ArcProps extends PathProps {
shape?: Partial<ArcShape>
}
class Arc extends Path<ArcProps> {
shape: ArcShape
constructor(opts?: ArcProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new ArcShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: ArcShape) {
const x = shape.cx;
const y = shape.cy;
const r = Math.max(shape.r, 0);
const startAngle = shape.startAngle;
const endAngle = shape.endAngle;
const clockwise = shape.clockwise;
const unitX = Math.cos(startAngle);
const unitY = Math.sin(startAngle);
ctx.moveTo(unitX * r + x, unitY * r + y);
ctx.arc(x, y, r, startAngle, endAngle, !clockwise);
}
}
Arc.prototype.type = 'arc';
export default Arc;
+138
View File
@@ -0,0 +1,138 @@
/**
* 贝塞尔曲线
*/
import Path, { PathProps } from '../Path';
import * as vec2 from '../../core/vector';
import {
quadraticSubdivide,
cubicSubdivide,
quadraticAt,
cubicAt,
quadraticDerivativeAt,
cubicDerivativeAt
} from '../../core/curve';
const out: number[] = [];
export class BezierCurveShape {
x1 = 0
y1 = 0
x2 = 0
y2 = 0
cpx1 = 0
cpy1 = 0
cpx2?: number
cpy2?: number
// Curve show percent, for animating
percent = 1
}
function someVectorAt(shape: BezierCurveShape, t: number, isTangent: boolean) {
const cpx2 = shape.cpx2;
const cpy2 = shape.cpy2;
if (cpx2 != null || cpy2 != null) {
return [
(isTangent ? cubicDerivativeAt : cubicAt)(shape.x1, shape.cpx1, shape.cpx2, shape.x2, t),
(isTangent ? cubicDerivativeAt : cubicAt)(shape.y1, shape.cpy1, shape.cpy2, shape.y2, t)
];
}
else {
return [
(isTangent ? quadraticDerivativeAt : quadraticAt)(shape.x1, shape.cpx1, shape.x2, t),
(isTangent ? quadraticDerivativeAt : quadraticAt)(shape.y1, shape.cpy1, shape.y2, t)
];
}
}
export interface BezierCurveProps extends PathProps {
shape?: Partial<BezierCurveShape>
}
class BezierCurve extends Path<BezierCurveProps> {
shape: BezierCurveShape
constructor(opts?: BezierCurveProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new BezierCurveShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: BezierCurveShape) {
let x1 = shape.x1;
let y1 = shape.y1;
let x2 = shape.x2;
let y2 = shape.y2;
let cpx1 = shape.cpx1;
let cpy1 = shape.cpy1;
let cpx2 = shape.cpx2;
let cpy2 = shape.cpy2;
let percent = shape.percent;
if (percent === 0) {
return;
}
ctx.moveTo(x1, y1);
if (cpx2 == null || cpy2 == null) {
if (percent < 1) {
quadraticSubdivide(x1, cpx1, x2, percent, out);
cpx1 = out[1];
x2 = out[2];
quadraticSubdivide(y1, cpy1, y2, percent, out);
cpy1 = out[1];
y2 = out[2];
}
ctx.quadraticCurveTo(
cpx1, cpy1,
x2, y2
);
}
else {
if (percent < 1) {
cubicSubdivide(x1, cpx1, cpx2, x2, percent, out);
cpx1 = out[1];
cpx2 = out[2];
x2 = out[3];
cubicSubdivide(y1, cpy1, cpy2, y2, percent, out);
cpy1 = out[1];
cpy2 = out[2];
y2 = out[3];
}
ctx.bezierCurveTo(
cpx1, cpy1,
cpx2, cpy2,
x2, y2
);
}
}
/**
* Get point at percent
*/
pointAt(t: number) {
return someVectorAt(this.shape, t, false);
}
/**
* Get tangent at percent
*/
tangentAt(t: number) {
const p = someVectorAt(this.shape, t, true);
return vec2.normalize(p, p);
}
};
BezierCurve.prototype.type = 'bezier-curve';
export default BezierCurve;
+38
View File
@@ -0,0 +1,38 @@
/**
* 圆形
*/
import Path, { PathProps } from '../Path';
export class CircleShape {
cx = 0
cy = 0
r = 0
}
export interface CircleProps extends PathProps {
shape?: Partial<CircleShape>
}
class Circle extends Path<CircleProps> {
shape: CircleShape
constructor(opts?: CircleProps) {
super(opts);
}
getDefaultShape() {
return new CircleShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: CircleShape) {
// Use moveTo to start a new sub path.
// Or it will be connected to other subpaths when in CompoundPath
ctx.moveTo(shape.cx + shape.r, shape.cy);
ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
}
};
Circle.prototype.type = 'circle';
export default Circle;
+58
View File
@@ -0,0 +1,58 @@
/**
* 水滴形状
*/
import Path, { PathProps } from '../Path';
export class DropletShape {
cx = 0
cy = 0
width = 0
height = 0
}
export interface DropletProps extends PathProps {
shape?: Partial<DropletShape>
}
class Droplet extends Path<DropletProps> {
shape: DropletShape
constructor(opts?: DropletProps) {
super(opts);
}
getDefaultShape() {
return new DropletShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: DropletShape) {
const x = shape.cx;
const y = shape.cy;
const a = shape.width;
const b = shape.height;
ctx.moveTo(x, y + a);
ctx.bezierCurveTo(
x + a,
y + a,
x + a * 3 / 2,
y - a / 3,
x,
y - b
);
ctx.bezierCurveTo(
x - a * 3 / 2,
y - a / 3,
x - a,
y + a,
x,
y + a
);
ctx.closePath();
}
}
Droplet.prototype.type = 'droplet';
export default Droplet;
+49
View File
@@ -0,0 +1,49 @@
/**
* 椭圆形状
*/
import Path, { PathProps } from '../Path';
export class EllipseShape {
cx = 0
cy = 0
rx = 0
ry = 0
}
export interface EllipseProps extends PathProps {
shape?: Partial<EllipseShape>
}
class Ellipse extends Path<EllipseProps> {
shape: EllipseShape
constructor(opts?: EllipseProps) {
super(opts);
}
getDefaultShape() {
return new EllipseShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: EllipseShape) {
const k = 0.5522848;
const x = shape.cx;
const y = shape.cy;
const a = shape.rx;
const b = shape.ry;
const ox = a * k; // 水平控制点偏移量
const oy = b * k; // 垂直控制点偏移量
// 从椭圆的左端点开始顺时针绘制四条三次贝塞尔曲线
ctx.moveTo(x - a, y);
ctx.bezierCurveTo(x - a, y - oy, x - ox, y - b, x, y - b);
ctx.bezierCurveTo(x + ox, y - b, x + a, y - oy, x + a, y);
ctx.bezierCurveTo(x + a, y + oy, x + ox, y + b, x, y + b);
ctx.bezierCurveTo(x - ox, y + b, x - a, y + oy, x - a, y);
ctx.closePath();
}
}
Ellipse.prototype.type = 'ellipse';
export default Ellipse;
+51
View File
@@ -0,0 +1,51 @@
/**
* 心形
*/
import Path, { PathProps } from '../Path';
export class HeartShape {
cx = 0
cy = 0
width = 0
height = 0
}
export interface HeartProps extends PathProps {
shape?: Partial<HeartShape>
}
class Heart extends Path<HeartProps> {
shape: HeartShape
constructor(opts?: HeartProps) {
super(opts);
}
getDefaultShape() {
return new HeartShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: HeartShape) {
const x = shape.cx;
const y = shape.cy;
const a = shape.width;
const b = shape.height;
ctx.moveTo(x, y);
ctx.bezierCurveTo(
x + a / 2, y - b * 2 / 3,
x + a * 2, y + b / 3,
x, y + b
);
ctx.bezierCurveTo(
x - a * 2, y + b / 3,
x - a / 2, y - b * 2 / 3,
x, y
);
}
}
Heart.prototype.type = 'heart';
export default Heart;
+60
View File
@@ -0,0 +1,60 @@
/**
* 正多边形
*/
import Path, { PathProps } from '../Path';
const PI = Math.PI;
const sin = Math.sin;
const cos = Math.cos;
export class IsogonShape {
x = 0
y = 0
r = 0
n = 0
}
export interface IsogonProps extends PathProps {
shape?: Partial<IsogonShape>
}
class Isogon extends Path<IsogonProps> {
shape: IsogonShape
constructor(opts?: IsogonProps) {
super(opts);
}
getDefaultShape() {
return new IsogonShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: IsogonShape) {
const n = shape.n;
if (!n || n < 2) {
return;
}
const x = shape.x;
const y = shape.y;
const r = shape.r;
const dStep = 2 * PI / n;
let deg = -PI / 2;
ctx.moveTo(x + r * cos(deg), y + r * sin(deg));
for (let i = 0, end = n - 1; i < end; i++) {
deg += dStep;
ctx.lineTo(x + r * cos(deg), y + r * sin(deg));
}
ctx.closePath();
return;
}
}
Isogon.prototype.type = 'isogon';
export default Isogon;
+96
View File
@@ -0,0 +1,96 @@
/**
* 直线
* @module zrender/graphic/shape/Line
*/
import Path, { PathProps } from '../Path';
import {subPixelOptimizeLine} from '../helper/subPixelOptimize';
import { VectorArray } from '../../core/vector';
// Avoid create repeatly.
const subPixelOptimizeOutputShape = {};
export class LineShape {
// Start point
x1 = 0
y1 = 0
// End point
x2 = 0
y2 = 0
percent = 1
}
export interface LineProps extends PathProps {
shape?: Partial<LineShape>
}
class Line extends Path<LineProps> {
shape: LineShape
constructor(opts?: LineProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new LineShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: LineShape) {
let x1;
let y1;
let x2;
let y2;
if (this.subPixelOptimize) {
const optimizedShape = subPixelOptimizeLine(
subPixelOptimizeOutputShape, shape, this.style
);
x1 = optimizedShape.x1;
y1 = optimizedShape.y1;
x2 = optimizedShape.x2;
y2 = optimizedShape.y2;
}
else {
x1 = shape.x1;
y1 = shape.y1;
x2 = shape.x2;
y2 = shape.y2;
}
const percent = shape.percent;
if (percent === 0) {
return;
}
ctx.moveTo(x1, y1);
if (percent < 1) {
x2 = x1 * (1 - percent) + x2 * percent;
y2 = y1 * (1 - percent) + y2 * percent;
}
ctx.lineTo(x2, y2);
}
/**
* Get point at percent
*/
pointAt(p: number): VectorArray {
const shape = this.shape;
return [
shape.x1 * (1 - p) + shape.x2 * p,
shape.y1 * (1 - p) + shape.y2 * p
];
}
}
Line.prototype.type = 'line';
export default Line;
+38
View File
@@ -0,0 +1,38 @@
/**
* 多边形
* @module zrender/shape/Polygon
*/
import Path, { PathProps } from '../Path';
import * as polyHelper from '../helper/poly';
import { VectorArray } from '../../core/vector';
export class PolygonShape {
points: VectorArray[] = null
smooth?: number = 0
smoothConstraint?: VectorArray[] = null
}
export interface PolygonProps extends PathProps {
shape?: Partial<PolygonShape>
}
class Polygon extends Path<PolygonProps> {
shape: PolygonShape
constructor(opts?: PolygonProps) {
super(opts);
}
getDefaultShape() {
return new PolygonShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: PolygonShape) {
polyHelper.buildPath(ctx, shape, true);
}
};
Polygon.prototype.type = 'polygon';
export default Polygon;
+45
View File
@@ -0,0 +1,45 @@
/**
* @module zrender/graphic/shape/Polyline
*/
import Path, { PathProps } from '../Path';
import * as polyHelper from '../helper/poly';
import { VectorArray } from '../../core/vector';
export class PolylineShape {
points: VectorArray[] = null
// Percent of displayed polyline. For animating purpose
percent?: number = 1
smooth?: number = 0
smoothConstraint?: VectorArray[] = null
}
export interface PolylineProps extends PathProps {
shape?: Partial<PolylineShape>
}
class Polyline extends Path<PolylineProps> {
shape: PolylineShape
constructor(opts?: PolylineProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new PolylineShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: PolylineShape) {
polyHelper.buildPath(ctx, shape, false);
}
}
Polyline.prototype.type = 'polyline';
export default Polyline;
+79
View File
@@ -0,0 +1,79 @@
/**
* 矩形
* @module zrender/graphic/shape/Rect
*/
import Path, { PathProps } from '../Path';
import * as roundRectHelper from '../helper/roundRect';
import {subPixelOptimizeRect} from '../helper/subPixelOptimize';
export class RectShape {
// 左上、右上、右下、左下角的半径依次为r1、r2、r3、r4
// r缩写为1 相当于 [1, 1, 1, 1]
// r缩写为[1] 相当于 [1, 1, 1, 1]
// r缩写为[1, 2] 相当于 [1, 2, 1, 2]
// r缩写为[1, 2, 3] 相当于 [1, 2, 3, 2]
r?: number | number[]
x = 0
y = 0
width = 0
height = 0
}
export interface RectProps extends PathProps {
shape?: Partial<RectShape>
}
// Avoid create repeatly.
const subPixelOptimizeOutputShape = {};
class Rect extends Path<RectProps> {
shape: RectShape
constructor(opts?: RectProps) {
super(opts);
}
getDefaultShape() {
return new RectShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RectShape) {
let x: number;
let y: number;
let width: number;
let height: number;
if (this.subPixelOptimize) {
const optimizedShape = subPixelOptimizeRect(subPixelOptimizeOutputShape, shape, this.style);
x = optimizedShape.x;
y = optimizedShape.y;
width = optimizedShape.width;
height = optimizedShape.height;
optimizedShape.r = shape.r;
shape = optimizedShape;
}
else {
x = shape.x;
y = shape.y;
width = shape.width;
height = shape.height;
}
if (!shape.r) {
ctx.rect(x, y, width, height);
}
else {
roundRectHelper.buildPath(ctx, shape);
}
}
isZeroArea() {
return !this.shape.width || !this.shape.height;
}
}
Rect.prototype.type = 'rect';
export default Rect;
+41
View File
@@ -0,0 +1,41 @@
/**
* 圆环
*/
import Path, { PathProps } from '../Path';
export class RingShape {
cx = 0
cy = 0
r = 0
r0 = 0
}
export interface RingProps extends PathProps {
shape?: Partial<RingShape>
}
class Ring extends Path<RingProps> {
shape: RingShape
constructor(opts?: RingProps) {
super(opts);
}
getDefaultShape() {
return new RingShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RingShape) {
const x = shape.cx;
const y = shape.cy;
const PI2 = Math.PI * 2;
ctx.moveTo(x + shape.r, y);
ctx.arc(x, y, shape.r, 0, PI2, false);
ctx.moveTo(x + shape.r0, y);
ctx.arc(x, y, shape.r0, 0, PI2, true);
}
}
Ring.prototype.type = 'ring';
export default Ring;
+76
View File
@@ -0,0 +1,76 @@
/**
* 玫瑰线
* @module zrender/graphic/shape/Rose
*/
import Path, { PathProps } from '../Path';
const sin = Math.sin;
const cos = Math.cos;
const radian = Math.PI / 180;
export class RoseShape {
cx = 0
cy = 0
r: number[] = []
k = 0
n = 1
}
export interface RoseProps extends PathProps {
shape?: Partial<RoseShape>
}
class Rose extends Path<RoseProps> {
shape: RoseShape
constructor(opts?: RoseProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new RoseShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RoseShape) {
const R = shape.r;
const k = shape.k;
const n = shape.n;
const x0 = shape.cx;
const y0 = shape.cy;
let x;
let y;
let r;
ctx.moveTo(x0, y0);
for (let i = 0, len = R.length; i < len; i++) {
r = R[i];
for (let j = 0; j <= 360 * n; j++) {
x = r
* sin(k / n * j % 360 * radian)
* cos(j * radian)
+ x0;
y = r
* sin(k / n * j % 360 * radian)
* sin(j * radian)
+ y0;
ctx.lineTo(x, y);
}
}
}
}
Rose.prototype.type = 'rose';
export default Rose;
+56
View File
@@ -0,0 +1,56 @@
import Path, { PathProps } from '../Path';
import * as roundSectorHelper from '../helper/roundSector';
export class SectorShape {
cx = 0
cy = 0
r0 = 0
r = 0
startAngle = 0
endAngle = Math.PI * 2
clockwise = true
/**
* Corner radius of sector
*
* clockwise, from inside to outside, four corners are
* inner start -> inner end
* outer start -> outer end
*
* 5 => [5, 5, 5, 5]
* [5] => [5, 5, 0, 0]
* [5, 10] => [5, 5, 10, 10]
* [5, 10, 15] => [5, 10, 15, 15]
* [5, 10, 15, 20] => [5, 10, 15, 20]
*/
cornerRadius: number | number[] = 0
}
export interface SectorProps extends PathProps {
shape?: Partial<SectorShape>
}
class Sector extends Path<SectorProps> {
shape: SectorShape
constructor(opts?: SectorProps) {
super(opts);
}
getDefaultShape() {
return new SectorShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: SectorShape) {
roundSectorHelper.buildPath(ctx, shape);
}
isZeroArea() {
return this.shape.startAngle === this.shape.endAngle
|| this.shape.r === this.shape.r0;
}
}
Sector.prototype.type = 'sector';
export default Sector;
+76
View File
@@ -0,0 +1,76 @@
/**
* n角星(n>3)
* @module zrender/graphic/shape/Star
*/
import Path, { PathProps } from '../Path';
const PI = Math.PI;
const cos = Math.cos;
const sin = Math.sin;
export class StarShape {
cx = 0
cy = 0
n = 3
r0: number
r = 0
}
export interface StarProps extends PathProps {
shape?: Partial<StarShape>
}
class Star extends Path<StarProps> {
shape: StarShape
constructor(opts?: StarProps) {
super(opts);
}
getDefaultShape() {
return new StarShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: StarShape) {
const n = shape.n;
if (!n || n < 2) {
return;
}
const x = shape.cx;
const y = shape.cy;
const r = shape.r;
let r0 = shape.r0;
// 如果未指定内部顶点外接圆半径,则自动计算
if (r0 == null) {
r0 = n > 4
// 相隔的外部顶点的连线的交点,
// 被取为内部交点,以此计算r0
? r * cos(2 * PI / n) / cos(PI / n)
// 二三四角星的特殊处理
: r / 3;
}
const dStep = PI / n;
let deg = -PI / 2;
const xStart = x + r * cos(deg);
const yStart = y + r * sin(deg);
deg += dStep;
// 记录边界点,用于判断inside
ctx.moveTo(xStart, yStart);
for (let i = 0, end = n * 2 - 1, ri; i < end; i++) {
ri = i % 2 === 0 ? r0 : r;
ctx.lineTo(x + ri * cos(deg), y + ri * sin(deg));
deg += dStep;
}
ctx.closePath();
}
}
Star.prototype.type = 'star';
export default Star;
+92
View File
@@ -0,0 +1,92 @@
/**
* 内外旋轮曲线
* @module zrender/graphic/shape/Trochold
*/
import Path, { PathProps } from '../Path';
const cos = Math.cos;
const sin = Math.sin;
export class TrochoidShape {
cx = 0
cy = 0
r = 0
r0 = 0
d = 0
location = 'out'
}
export interface TrochoidProps extends PathProps {
shape?: Partial<TrochoidShape>
}
class Trochoid extends Path<TrochoidProps> {
shape: TrochoidShape
constructor(opts?: TrochoidProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new TrochoidShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: TrochoidShape) {
const R = shape.r;
const r = shape.r0;
const d = shape.d;
const offsetX = shape.cx;
const offsetY = shape.cy;
const delta = shape.location === 'out' ? 1 : -1;
let x1;
let y1;
let x2;
let y2;
if (shape.location && R <= r) {
return;
}
let num = 0;
let i = 1;
let theta;
x1 = (R + delta * r) * cos(0)
- delta * d * cos(0) + offsetX;
y1 = (R + delta * r) * sin(0)
- d * sin(0) + offsetY;
ctx.moveTo(x1, y1);
// 计算结束时的i
do {
num++;
}
while ((r * num) % (R + delta * r) !== 0);
do {
theta = Math.PI / 180 * i;
x2 = (R + delta * r) * cos(theta)
- delta * d * cos((R / r + delta) * theta)
+ offsetX;
y2 = (R + delta * r) * sin(theta)
- d * sin((R / r + delta) * theta)
+ offsetY;
ctx.lineTo(x2, y2);
i++;
}
while (i <= (r * num) / (R + delta * r) * 360);
}
}
Trochoid.prototype.type = 'trochoid';
export default Trochoid;