import {GameDrawable} from "./GameDrawable.js";
import {Point,randomColor} from "../Misc.js"
// const Misc = require("../Misc.js")
// const Point = Misc.Point
/**
* @typedef {"rectangle" | "oval" | "polygon" | "line"} TYPE
*/
const shapes = ["rectangle","oval","polygon","line"]
/**
* Shape drawable class. Draws rectangles, ovals, polygons and lines
* @extends GameDrawable
*
* @property {TYPE} type Type of shape: rectangle, oval, polygon, line
* @property {string} fill Fill of the shape. Can be CSS color or "random"
* @property {string} stroke Stroke of the shape. Can be CSS color or "random"
* @property {number} lineWidth Width of the line/outline
* @property {number} rx Radius of oval on X axis
* @property {number} ry Radius of oval on Y axis
* @property {Array<number>} coords Array of points in line/polygon in format [x1,y1,...,xn,yn]
*/
class GameShape extends GameDrawable {
#type;
set type(newType) {
if (!shapes.includes(newType)) {
throw new Error(`Incorrect type name. Should be of "${shapes}", is ${newType}`)
}
this.#type = newType
}
get type() {
return this.#type
}
#fill;
set fill(newFill) {
if (newFill === "random") {
this.#fill = randomColor()
return
}
this.#fill = newFill
}
get fill() {
return this.#fill
}
#stroke;
set stroke(newStroke) {
if (newStroke === "random") {
this.#stroke = randomColor()
return
}
this.#stroke = newStroke
}
get stroke() {
return this.#stroke
}
#lineWidth;
set lineWidth(newLineWidth) {
if (newLineWidth === undefined) {
this.#lineWidth = 0
return
}
if (isNaN(newLineWidth)) {
throw new Error("Line width value is not a number!")
}
this.#lineWidth = newLineWidth
}
get lineWidth() {
return this.#lineWidth
}
/**
* Constructor of Shape drawable
* @param {TYPE} type Type of shape: rectangle, oval, polygon, line
* @param {Object} attrs Attribute object
*/
constructor(type='rectangle',attrs={}) {
super(attrs)
if (type === "undefined") {
this.type = attrs.type
this.fill = attrs.fill
this.stroke = attrs.stroke
this.lineWidth = attrs.lineWidth
this.rx = attrs.rx
this.ry = attrs.ry
this.coords = attrs.coords
return
}
this.type = type;
this.fill = attrs.fill
this.stroke = attrs.stroke
this.lineWidth = attrs.lineWidth
if (type === 'rectangle') {
this.width = this.width || 100
this.height = this.height || 100
}
else if (type === 'oval') {
this.rx = attrs.rx || attrs.ry || 50
this.ry = attrs.ry || attrs.rx || 50
}
else if (["polygon","line"].includes(type)) {
const coords = attrs.coords || this.parsePath(attrs.path) || []
if (!Array.isArray(coords) || coords.some(x => isNaN(x))) {
throw new Error("Wrong type of coords!")
}
this.coords = coords || []
}
if (!this.fill && !this.stroke) {
if (this.type === "line") {
this.stroke = "red"
} else {
this.fill = 'red'
}
}
}
/**
* Parses path in format "x y command number c n c n ..."
* @param {string} path
* @returns {Array<Number>}
*/
parsePath(path) {
if (path === undefined) {
return []
}
const pathArray = path.trim().split(/\s+/)
const retPath = []
function getNumber() {
const raw = pathArray.shift()
const value = parseFloat(raw)
if (isNaN(value)) {
throw new Error("Invalid number in path! " + raw)
}
return value
}
function getLetter() {
const value = pathArray.shift()
if (!"fbrl".includes(value)) {
throw new Error("Invalid letter in path! " + value)
}
return value
}
function move(dist) {
let newPoint = lastPoint.add(new Point(dist,0))
newPoint = newPoint.rotateAround(lastPoint,rotation)
return newPoint
}
function rotate(angle) {
rotation += angle * (Math.PI / 180)
}
let lastPoint
try {
lastPoint = new Point(getNumber(), getNumber())
} catch (e) {
throw new Error("Path is not in correct format, it has to start with \"x y\" as values (0 0 for example)!")
}
retPath.push(lastPoint)
let rotation = 0
while (pathArray.length) {
const command = getLetter()
const number = getNumber()
if (command === "f") {
lastPoint = move(number)
retPath.push(lastPoint)
}
else if (command === "b") {
lastPoint = move(-number)
retPath.push(lastPoint)
}
else if (command === "l") {
rotate(-number)
}
else if (command === "r") {
rotate(number)
}
else {
throw new Error(`Unknown command "${command}"!`)
}
}
return retPath.map(p => p.asArray()).flat(1)
}
/**
* Sets coords from path
* @param {string} path
*/
setPath(path) {
this.coords = this.parsePath(path)
}
/**
* Replaces coords array property with two points
* @param {Point} from Start Point
* @param {Point} to End Point
*/
setLine(from,to) {
if (!(from instanceof Point) || !(to instanceof Point)) {
throw new Error("From and to have to be Point objects!")
}
this.coords = [...from.asArray(),...to.asArray()]
}
/**
* Appends Point to the coords array
* @param {Point} point
*/
addPoint(point) {
if (this.coords === undefined) {
throw "Undefined coords!"
}
this.coords.push(...point.asArray())
}
/**
* Called when drawing
* @param {CanvasRenderingContext2D} ctx Rendering context on which the method draws
*/
drawFunction(ctx) {
ctx.color = this.fill
if (this.type === 'rectangle') {
if (this.fill !== undefined) {
ctx.fillStyle = this.fill;
ctx.fillRect(- (this.width / 2), - (this.height / 2), this.width, this.height)
}
if (this.stroke !== undefined) {
ctx.strokeStyle = this.stroke
ctx.lineWidth = this.lineWidth
ctx.strokeRect(- (this.width / 2), - (this.height / 2), this.width, this.height)
}
return
}
else if (this.type === 'oval') {
ctx.beginPath();
ctx.ellipse(0,0,this.rx,this.ry,0,0,2*Math.PI);
}
else if (this.type === 'polygon' || this.type === 'line') {
ctx.beginPath();
ctx.moveTo(this.coords[0],this.coords[1])
for (let i = 2; i < this.coords.length-1; i+=2) {
ctx.lineTo(this.coords[i],this.coords[i+1])
}
if (this.type === 'polygon') {
ctx.closePath()
}
}
if (this.type !== "line" && this.fill !== undefined) {
ctx.fillStyle = this.fill;
ctx.fill();
}
if (this.stroke !== undefined) {
ctx.strokeStyle = this.stroke
ctx.lineWidth = this.lineWidth
ctx.stroke()
}
}
/**
* Returns object of attributes of current instance.
* @returns {Object} Attribute object.
*/
getAttrs() {
return Object.assign({
type : this.type,
fill : this.fill,
stroke : this.stroke,
lineWidth : this.lineWidth,
rx : this.rx,
ry : this.ry,
coords : (this.coords === undefined) ? undefined : [...this.coords],
},super.getAttrs())
}
/**
* Returns copy of current instance.
* @param {string} newName Name of the newly created instance. Names have to be unique.
* @returns {GameShape} New instance of GameShape with the same attributes.
*/
copy(newName) {
const attrs = this.getAttrs()
if (newName === undefined) {
attrs.name = (attrs.name === undefined) ? undefined : attrs.name + "_copy"
} else {
attrs.name = newName
}
return new GameShape("undefined",attrs)
}
}
function isColor(strColor) {
const s = new Option().style;
s.color = strColor;
return s.color !== '';
}
export {GameShape}