UNPKG

pencil.js

Version:

Nice modular interactive 2D drawing library.

376 lines (340 loc) 12.2 kB
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; } }