modules_component_component.js
import Container from "@pencil.js/container";
import Position from "@pencil.js/position";
import OffScreenCanvas from "@pencil.js/offscreen-canvas";
/**
* @module Component
*/
/**
* Abstract class for visual component
* @abstract
* @class
* @extends {module:Container}
*/
export default class Component extends Container {
/**
* Component constructor
* @param {PositionDefinition} positionDefinition - Position in space
* @param {ComponentOptions} [options] - Drawing options
*/
constructor (positionDefinition, options = {}) {
super(positionDefinition, options);
/**
* @type {Path2D}
*/
this.path = null;
/**
* @type {Boolean}
*/
this.isClicked = false;
/**
* @type {Boolean}
*/
this.isHovered = false;
}
/**
* Return the relative origin position of draw
* @return {Position}
*/
getOrigin () {
return Position.from(this.options.origin);
}
/**
* Define options for this component
* @param {ComponentOptions} [options={}] - Options to override
* @return {Component} Itself
*/
setOptions (options = {}) {
const { shadow } = this.options;
super.setOptions(options);
if (typeof this.options.origin !== "string") {
this.options.origin = Position.from(this.options.origin);
}
this.options.shadow = {
...Component.defaultOptions.shadow, // default
...shadow, // previous value
...options.shadow, // override value
};
this.options.shadow.position = Position.from(this.options.shadow.position);
return this;
}
/**
* Tell if a component will need to be filled
* @return {Boolean}
*/
get willFill () {
return Boolean(this.options.fill);
}
/**
* Tell if a component will need to be stroked
* @return {*|boolean}
*/
get willStroke () {
return Boolean(this.options.stroke) && this.options.strokeWidth > 0;
}
/**
* @inheritDoc
* @return {Component} Itself
*/
setContext (ctx) {
super.setContext(ctx);
if (this.options.shadow.color) {
const shadowPosition = Position.from(this.options.shadow.position);
ctx.shadowColor = this.options.shadow.color.toString();
ctx.shadowBlur = this.options.shadow.blur;
ctx.shadowOffsetX = shadowPosition.x;
ctx.shadowOffsetY = shadowPosition.y;
}
else {
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
if (this.willFill) {
ctx.fillStyle = this.options.fill.toString(ctx);
}
if (this.willStroke) {
ctx.lineJoin = this.options.join;
ctx.lineCap = this.options.cap;
ctx.strokeStyle = this.options.stroke.toString(ctx);
ctx.lineWidth = this.options.strokeWidth;
if (this.options.dashed) {
const { dashed, strokeWidth } = this.options;
const pattern = Array.isArray(dashed) ? dashed : Component.dashes.default;
ctx.setLineDash(pattern.map(v => v * strokeWidth));
}
else {
ctx.setLineDash([]);
}
}
return this;
}
/**
* Make the path and trace it
* @param {CanvasRenderingContext2D} ctx - Drawing context
* @return {Component} Itself
*/
makePath (ctx) {
if (this.willFill || this.willStroke) {
const origin = this.getOrigin();
ctx.translate(origin.x, origin.y);
this.path = new window.Path2D();
this.trace(this.path);
if (this.willFill) {
ctx.fill(this.path);
}
if (this.willStroke) {
ctx.stroke(this.path);
}
ctx.translate(-origin.x, -origin.y);
}
return this;
}
/**
* Every component should have a trace function
* @throws {ReferenceError}
*/
trace () {
throw new ReferenceError(`Unimplemented [trace] function in ${this.constructor.name}.`);
}
/**
* Define if is hover a position
* @param {PositionDefinition} positionDefinition - Any position
* @param {CanvasRenderingContext2D} [ctx] - Context use for calculation
* @return {Boolean}
*/
isHover (positionDefinition, ctx = OffScreenCanvas.getDrawingContext()) {
if (!this.options.shown) {
return false;
}
ctx.save();
const position = Position.from(positionDefinition);
const origin = this.getOrigin();
ctx.translate(origin.x, origin.y);
if (!this.willFill && !this.willStroke) {
ctx.restore();
return false;
}
if (!this.path) {
this.path = new window.Path2D();
this.trace(this.path);
}
let result = (this.willFill && ctx.isPointInPath(this.path, position.x, position.y)) ||
(this.willStroke && ctx.isPointInStroke(this.path, position.x, position.y));
if (this.options.clip) {
result = result && this.options.clip.isHover(position, ctx);
}
ctx.restore();
return result;
}
/**
* @typedef {Object} ShadowOptions
* @prop {Number} [blur=0] - Spread of the shadow around the component
* @prop {PositionDefinition} [position=new Position()] - Position of the shadow relative to the component
* @prop {String|ColorDefinition} [color=null] - Color of the shadow
*/
/**
* @typedef {Object} ComponentOptions
* @extends ContainerOptions
* @prop {String|ColorDefinition} [fill="#000"] - Color used to fill, set to null for transparent
* @prop {String|ColorDefinition} [stroke=null] - Color used to stroke, set to null for transparent
* @prop {Number} [strokeWidth=2] - Stroke line thickness in pixels
* @prop {Boolean|Array} [dashed=false] - Should the line be dashed, you can also specify the dash pattern (ex: [4, 4] or Component.dashes.dots)
* @prop {String} [cursor=Component.cursors.default] - Cursor to use when hover
* @prop {String} [join=Component.joins.miter] - How lines join between them
* @prop {PositionDefinition} [origin=new Position()] - Relative offset
* @prop {ShadowOptions} [shadow] - Set of options to set a shadow
*/
/**
* @type {ComponentOptions}
*/
static get defaultOptions () {
return {
...super.defaultOptions,
fill: "#000",
stroke: null,
strokeWidth: 2,
dashed: false,
cursor: Component.cursors.default,
join: Component.joins.miter,
origin: new Position(),
shadow: {
blur: 0,
position: new Position(),
color: null,
},
};
}
/**
* @typedef {Object} Cursors
* @prop {String} default - Normal behavior
* @prop {String} none - No visible cursor
* @prop {String} contextMenu - Possible to extends context-menu
* @prop {String} help - Display help
* @prop {String} pointer - Can be clicked
* @prop {String} progress - Process running in background
* @prop {String} wait - Process running in foreground
* @prop {String} cell - Table cell selection
* @prop {String} crosshair - Precise selection
* @prop {String} text - Text selection
* @prop {String} textVertical - Vertical test selection
* @prop {String} alias - Can create a shortcut
* @prop {String} copy - Can be copied
* @prop {String} move - Move around
* @prop {String} noDrop - Drop not allowed
* @prop {String} notAllowed - Action not allowed
* @prop {String} grab - Can be grabbed
* @prop {String} grabbing - Currently grabbing
* @prop {String} allScroll - Scroll in all direction
* @prop {String} colResize - Horizontal resize
* @prop {String} rowResize - Vertical resize
* @prop {String} nResize - Resize the north border
* @prop {String} eResize - Resize the east border
* @prop {String} sResize - Resize the south border
* @prop {String} wResize - Resize the west border
* @prop {String} neResize - Resize the north-east corner
* @prop {String} seResize - Resize the south-east corner
* @prop {String} swResize - Resize the north-west corner
* @prop {String} nwResize - Resize the north-west corner
* @prop {String} ewResize - Horizontal resize
* @prop {String} nsResize - Vertical resize
* @prop {String} neswResize - Diagonal resize (top-right to bottom-left)
* @prop {String} nwseResize - Diagonal resize (top-left to bottom-right)
* @prop {String} zoomIn - Zoom in
* @prop {String} zoomOut - Zoom out
* @prop {String} link - Alias for "alias"
* @prop {String} verticalResize - Alias for "rowResize"
* @prop {String} horizontalResize - Alias for "colResize"
* @prop {String} topResize - Alias for "nResize"
* @prop {String} rightResize - Alias for "eResize"
* @prop {String} bottomResize - Alias for "sResize"
* @prop {String} leftResize - Alias for "wResize"
*/
/**
* All available cursors
* {@link https://www.w3.org/TR/2017/WD-css-ui-4-20171222/#cursor}
* @type {Cursors}
*/
static get cursors () {
const cursors = {
default: "default",
none: "none",
contextMenu: "context-menu",
help: "help",
pointer: "pointer",
progress: "progress",
wait: "wait",
cell: "cell",
crosshair: "crosshair",
text: "text",
textVertical: "vertical-text",
alias: "alias",
copy: "copy",
move: "move",
noDrop: "no-drop",
notAllowed: "not-allowed",
grab: "grab",
grabbing: "grabbing",
allScroll: "all-scroll",
colResize: "col-resize",
rowResize: "row-resize",
nResize: "n-resize",
eResize: "e-resize",
sResize: "s-resize",
wResize: "w-resize",
neResize: "ne-resize",
seResize: "se-resize",
swResize: "sw-resize",
nwResize: "nw-resize",
ewResize: "ew-resize",
nsResize: "ns-resize",
neswResize: "nesw-resize",
nwseResize: "nwse-resize",
zoomIn: "zoom-in",
zoomOut: "zoom-out",
};
// Aliases
cursors.link = cursors.alias;
cursors.verticalResize = cursors.rowResize;
cursors.horizontalResize = cursors.colResize;
cursors.topResize = cursors.nResize;
cursors.rightResize = cursors.eResize;
cursors.bottomResize = cursors.sResize;
cursors.leftResize = cursors.wResize;
return cursors;
}
/**
* @typedef {Object} LineJoins
* @prop {String} miter - Join segment by extending the line edges until they meet
* @prop {String} round - Join with a circle tangent to line edges
* @prop {String} bevel - Join with a straight line between the line edges
*/
/**
* @type {LineJoins}
*/
static get joins () {
return {
miter: "miter",
round: "round",
bevel: "bevel",
};
}
/**
* @typedef {Object} DashPatterns
* @prop {Array<Number>} default - Simple dash
* @prop {Array<Number>} dots - Simple dots
* @prop {Array<Number>} long - Longer dash
* @prop {Array<Number>} sewing - Alternating pattern of short and long dash
*/
/**
* @type {DashPatterns}
*/
static get dashes () {
const pattern = {
default: [4, 4],
dots: [1, 4],
long: [9, 4],
};
pattern.sewing = [...pattern.default, ...pattern.dots];
return pattern;
}
}