UNPKG

@webwriter/geometry-cloze

Version:

Create and view geometry exercises with coloring, styling and labeling options.

475 lines (451 loc) 12.7 kB
import { ContextMenuItem } from '../../../types/ContextMenu'; import Manager from '../../CanvasManager/Abstracts'; import Element, { NamedElement } from './Element'; export interface StylableData { lineWidth?: number; size?: number; stroke?: string; fill?: string; shadow?: boolean; labelColor?: string; showLabel?: boolean; labelStyle?: 'value' | 'name'; labelName?: string; dashed?: boolean; } const DEFAULT_STYLE = { lineWidth: 3, size: 10, stroke: '#111827', fill: 'transparent', shadow: false, showLabel: false, labelColor: '#111827', labelStyle: 'value', labelName: 'α', dashed: false } as const; export default class Stylable extends Element { public static COLORS = [ { label: 'Black', color: '#111827' }, { label: 'Red', color: '#dc2626' }, { label: 'Orange', color: '#ea580c' }, { label: 'Yellow', color: '#facc15' }, { label: 'Lime', color: '#84cc16' }, { label: 'Green', color: '#15803d' }, { label: 'Cyan', color: '#06b6d4' }, { label: 'Blue', color: '#2563eb' }, { label: 'Violet', color: '#6d28d9' }, { label: 'Pink', color: '#db2777' } ]; public static COLORS_WITH_TRANSPARENT = [ { label: 'Transparent', color: 'transparent' }, ...Stylable.COLORS ]; public static LETTERS = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' ]; public static GREEK = [ 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ω', 'ϡ', 'ͳ', 'ϸ' ]; private _lineWidth: number; private _size: number; private _stroke: string; private _fill: string; private _shadow: boolean; private _showLabel: boolean; private _labelColor: string; private _labelStyle: 'value' | 'name'; private _labelName: string; private _dashed: boolean; constructor(manager: Manager, data: StylableData & NamedElement = {}) { super(manager, data); this._lineWidth = data.lineWidth || DEFAULT_STYLE.lineWidth; this._size = data.size || DEFAULT_STYLE.size; this._stroke = data.stroke || DEFAULT_STYLE.stroke; this._fill = data.fill || DEFAULT_STYLE.fill; this._shadow = data.shadow || DEFAULT_STYLE.shadow; this._showLabel = data.showLabel || DEFAULT_STYLE.showLabel; this._labelColor = data.labelColor || DEFAULT_STYLE.labelColor; this._labelStyle = data.labelStyle || DEFAULT_STYLE.labelStyle; this._labelName = data.labelName || DEFAULT_STYLE.labelName; this._dashed = data.dashed || DEFAULT_STYLE.dashed; this.addEventListener('style-change', this.requestRedraw.bind(this)); } draw(ctx: CanvasRenderingContext2D) { super.draw(ctx); ctx.lineWidth = this.lineWidth; ctx.strokeStyle = this.stroke; ctx.fillStyle = this.fill; ctx.setLineDash(this.dashed ? [10, 10] : []); ctx.shadowBlur = this.shadow ? 5 : 0; ctx.shadowColor = this.shadow ? '#00000050' : 'transparent'; ctx.shadowOffsetX = this.shadow ? 5 : 0; ctx.shadowOffsetY = this.shadow ? 5 : 0; } setLineWidth(newLineWidth: number | null) { const newValue = newLineWidth ?? 3; const hasChanges = newValue !== this._lineWidth; this._lineWidth = newValue; if (hasChanges) this.fireEvent('style-change', { lineWidth: this.lineWidth }); } get lineWidth() { return this._lineWidth; } setSize(size: number | null) { const newValue = size ?? 10; const hasChanges = newValue !== this._size; this._size = newValue; if (hasChanges) this.fireEvent('style-change', { size: this.size }); } get size() { return this._size; } setStroke(stroke: string | null) { const newValue = stroke ?? 'transparent'; const hasChanges = newValue !== this._stroke; this._stroke = newValue; if (hasChanges) this.fireEvent('style-change', { stroke: this.stroke }); } get stroke() { return this._stroke; } setFill(fill: string | null) { const newValue = fill ?? 'transparent'; const hasChanges = newValue !== this._fill; this._fill = newValue; if (hasChanges) this.fireEvent('style-change', { fill: this.fill }); } get fill() { return this._fill; } setShadow(shadow: boolean | null) { const newValue = shadow ?? false; const hasChanges = newValue !== this._shadow; this._shadow = newValue; if (hasChanges) this.fireEvent('style-change', { shadow: this.shadow }); } get shadow() { return this._shadow; } setDashed(dashed: boolean | null) { const newValue = dashed ?? false; const hasChanges = newValue !== this._dashed; this._dashed = newValue; if (hasChanges) this.fireEvent('style-change', { dashed: this._dashed }); } get dashed() { return this._dashed; } shouldShowLabel(show: boolean | null) { const newValue = show ?? false; const hasChanges = newValue !== this._showLabel; this._showLabel = newValue; if (hasChanges) this.fireEvent('style-change', { showLabel: this._showLabel }); } get showLabel() { return this._showLabel; } setLabelColor(color: string | null) { const newValue = color ?? '#111827'; const hasChanges = newValue !== this._labelColor; this._labelColor = newValue; if (hasChanges) this.fireEvent('style-change', { labelColor: this._labelColor }); } get labelColor() { return this._labelColor; } setLabelStyle(style: 'value' | 'name' | null) { const newValue = style ?? 'value'; const hasChanges = newValue !== this._labelStyle; this._labelStyle = newValue; if (hasChanges) this.fireEvent('style-change', { labelStyle: this._labelStyle }); } get labelStyle() { return this._labelStyle; } setLabelName(name: string | null) { const newValue = name ?? 'α'; const hasChanges = newValue !== this._labelName; this._labelName = newValue; this.setLabelStyle('name'); if (hasChanges) this.fireEvent('style-change', { labelName: this._labelName }); } get labelName() { return this._labelName; } protected getValueLabel(): string { return ''; } protected getLabel(): string { if (this.labelStyle === 'value') return this.getValueLabel(); return this.labelName; } protected getStyleContextMenuItems(options: { stroke?: boolean; fill?: boolean; lineWidth?: boolean; showLabel?: boolean; dashed?: boolean; nameList?: 'lowercase' | 'uppercase' | 'greek'; }): ContextMenuItem[] { const res: ContextMenuItem[] = []; if (options.stroke) { res.push({ type: 'submenu', label: 'Stoke', key: 'stroke', items: Stylable.COLORS.map( (option) => ({ type: 'checkbox', label: option.label, getChecked: () => this._stroke === option.color, action: () => this.setStroke(option.color), key: `stroke_${option.label.toLowerCase()}` }) as const ) }); } if (options.fill) { res.push({ type: 'submenu', label: 'Fill', key: 'fill', items: Stylable.COLORS_WITH_TRANSPARENT.map((option) => { const color = option.color === 'transparent' ? option.color : option.color + '50'; return { type: 'checkbox', getChecked: () => this._fill === color, label: option.label, action: () => this.setFill(color), key: `fill_${option.label.toLowerCase()}` } as const; }) }); } if (options.lineWidth) { const options = [ { label: 'Extra thin', value: 1 }, { label: 'Thin', value: 2 }, { label: 'Medium', value: 3 }, { label: 'Thick', value: 5 }, { label: 'Extra Thick', value: 7 } ]; res.push({ type: 'submenu', label: 'Line Width', key: 'line_width', items: options.map( (option) => ({ type: 'checkbox', getChecked: () => this._lineWidth === option.value, label: option.label, action: () => this.setLineWidth(option.value), key: `line-width_${option.label.toLowerCase()}` }) as const ) }); } if (options.dashed) { res.push({ type: 'checkbox', label: 'Dashed', getChecked: () => this._dashed, action: () => this.setDashed(!this._dashed), key: 'dashed' }); } if (options.showLabel ?? this.getValueLabel() !== '') { res.push({ type: 'submenu', label: 'Label', key: 'label', items: [ { type: 'checkbox', label: 'Show Label', getChecked: () => this.showLabel, action: () => this.shouldShowLabel(!this.showLabel), key: 'show-label' }, { type: 'submenu', label: 'Color', key: 'label_color', items: Stylable.COLORS.map( (option) => ({ type: 'checkbox', getChecked: () => this._labelColor === option.color, label: option.label, action: () => this.setLabelColor(option.color), key: `label_color_${option.label.toLowerCase()}` }) as const ) }, { type: 'submenu', label: 'Name', key: 'label_name', items: [ { type: 'checkbox', key: 'label_name_value', label: 'Value', action: () => this.setLabelStyle('value'), getChecked: () => this._labelStyle === 'value' }, ...(options.nameList === 'greek' ? Stylable.GREEK : Stylable.LETTERS ).map((letter) => { if (options.nameList === 'uppercase') letter = letter.toUpperCase(); return { type: 'checkbox', getChecked: () => this._labelStyle === 'name' && this._labelName === letter, label: letter, action: () => { this.shouldShowLabel(true); this.setLabelName(letter); }, key: `label_color_${letter}` } as const; }) ] } ] }); } return res; } public getContextMenuItems(): ContextMenuItem[] { return [...super.getContextMenuItems()]; } public export() { const res: StylableData = {}; if (this._lineWidth !== DEFAULT_STYLE.lineWidth) res.lineWidth = this._lineWidth; if (this._size !== DEFAULT_STYLE.size) res.size = this._size; if (this._stroke !== DEFAULT_STYLE.stroke) res.stroke = this._stroke; if (this._fill !== DEFAULT_STYLE.fill) res.fill = this._fill; if (this._shadow !== DEFAULT_STYLE.shadow) res.shadow = this._shadow; if (this._showLabel !== DEFAULT_STYLE.showLabel) res.showLabel = this._showLabel; if (this._labelColor !== DEFAULT_STYLE.labelColor) res.labelColor = this._labelColor; if (this._labelStyle !== DEFAULT_STYLE.labelStyle) res.labelStyle = this._labelStyle; if (this._labelName !== DEFAULT_STYLE.labelName) res.labelName = this._labelName; if (this._dashed !== DEFAULT_STYLE.dashed) res.dashed = this._dashed; return { ...super.export(), ...res }; } }