diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c889ad..9e7f9022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - added global raycast function and raycast method to level - added support for textured polygons +- added support for concave polygon drawing - added `loadMusic()` to load streaming audio (doesn't block in loading screen) - added support for arrays in uniforms - added support for texture larger than 2048x2048 diff --git a/examples/polygon.js b/examples/polygon.js index d1b6756b..81167aa4 100644 --- a/examples/polygon.js +++ b/examples/polygon.js @@ -15,6 +15,8 @@ const poly = add([ vec2(80, 80), vec2(-60, 120), vec2(-120, 0), + vec2(-200, 20), + vec2(-270, 60), ], { colors: [ rgb(128, 255, 128), @@ -22,6 +24,8 @@ const poly = add([ rgb(128, 128, 255), rgb(255, 128, 128), rgb(128, 128, 128), + rgb(128, 255, 128), + rgb(255, 128, 128), ], }), pos(300, 300), @@ -33,6 +37,16 @@ let dragging = null; let hovering = null; poly.onDraw(() => { + const triangles = triangulate(poly.pts) + for (const triangle of triangles) { + drawTriangle({ + p1: triangle[0], + p2: triangle[1], + p3: triangle[2], + fill: false, + outline: { color: BLACK } + }) + } if (hovering !== null) { drawCircle({ pos: poly.pts[hovering], @@ -41,6 +55,15 @@ poly.onDraw(() => { } }); +onUpdate(()=>{ + if (isConvex(poly.pts)) { + poly.color = WHITE + } + else { + poly.color = rgb(192, 192, 192) + } +}) + onMousePress(() => { dragging = hovering; }); diff --git a/src/kaboom.ts b/src/kaboom.ts index 07ed5d4a..bcf399df 100644 --- a/src/kaboom.ts +++ b/src/kaboom.ts @@ -86,6 +86,8 @@ import { Vec2, vec2, wave, + isConvex, + triangulate, } from "./math"; import easings from "./easings"; @@ -2196,9 +2198,17 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { })); // TODO: better triangulation - const indices = [...Array(npts - 2).keys()] + let indices; + + if (opt.triangulate && isConvex(opt.pts)) { + const triangles = triangulate(opt.pts) + indices = triangles.map(t => t.map(p => opt.pts.indexOf(p))).flat() + } + else { + indices = [...Array(npts - 2).keys()] .map((n) => [0, n + 1, n + 2]) .flat(); + } drawRaw( verts, @@ -3859,8 +3869,10 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { }, inspect() { - return `(${(this.area.scale.x).toFixed(1)}, ${(this.area.scale.y).toFixed(1)})`; - } + return `(${this.area.scale.x.toFixed(1)}, ${ + this.area.scale.y.toFixed(1) + })`; + }, }; } @@ -6848,6 +6860,8 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { testCirclePolygon, testLinePoint, testLineCircle, + isConvex, + triangulate, // raw draw drawSprite, drawText, diff --git a/src/math.ts b/src/math.ts index 2cc10da6..ef63798b 100644 --- a/src/math.ts +++ b/src/math.ts @@ -2334,3 +2334,151 @@ export function sat(p1: Polygon, p2: Polygon): Vec2 | null { } return displacement; } + +// true if the angle is oriented counter clockwise +function isOrientedCcw(a: Vec2, b: Vec2, c: Vec2) { + // return det(b-a, c-a) >= 0 + return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) >= 0; +} + +// true if the polygon is oriented counter clockwise +function isOrientedCcwPolygon(polygon: Vec2[]) { + let total =0; + let prev:Vec2 = polygon[polygon.length-1]; + for (let i = 0; i < polygon.length; i++) { + total += (polygon[i].x - prev.x) * (polygon[i].y + prev.y); + prev = polygon[i]; + } + return total < 0; +} + +// true if a and b are on the same side of the line c->d +function onSameSide(a: Vec2, b: Vec2, c: Vec2, d: Vec2) { + const px = d.x - c.x, py = d.y - c.y; + // return det(p, a-c) * det(p, b-c) >= 0 + const l = px * (a.y - c.y) - py * (a.x - c.x); + const m = px * (b.y - c.y) - py * (b.x - c.x); + return l * m >= 0; +} + +// true if p is contained in the triangle abc +function pointInTriangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) { + return onSameSide(p, a, b, c) && onSameSide(p, b, a, c) + && onSameSide(p, c, a, b); +} + +// true if any vertex in the list `vertices' is in the triangle abc. +function someInTriangle(vertices: Vec2[], a: Vec2, b: Vec2, c: Vec2) { + for (const p of vertices) { + if ( + (p !== a) && (p !== b) && (p !== c) && pointInTriangle(p, a, b, c) + ) { + return true; + } + } + + return false; +} + +// true if the triangle is an ear, which is whether it can be cut off from the polygon without leaving a hole behind +function isEar(a: Vec2, b: Vec2, c: Vec2, vertices: Vec2[]) { + return isOrientedCcw(a, b, c) && !someInTriangle(vertices, a, b, c); +} + +export function triangulate(pts: Vec2[]): Vec2[][] { + if (pts.length < 3) { + return []; + } + if (pts.length == 3) { + return [pts]; + } + + /* Create a list of indexes to the previous and next points of a given point + prev_idx[i] gives the index to the previous point of the point at i */ + let nextIdx = []; + let prevIdx = []; + let idx = 0; + for (let i = 0; i < pts.length; i++) { + const lm = pts[idx]; + const pt = pts[i]; + if (pt.x < lm.x || (pt.x == lm.x && pt.y < lm.y)) { + idx = idx; + } + nextIdx[i] = i + 1; + prevIdx[i] = i - 1; + } + nextIdx[nextIdx.length - 1] = 0; + prevIdx[0] = prevIdx.length - 1; + + // If the polygon is not counter clockwise, swap the lists, thus reversing the winding + if (!isOrientedCcwPolygon(pts)) { + [nextIdx, prevIdx] = [prevIdx, nextIdx]; + } + + const concaveVertices = []; + for (let i = 0; i < pts.length; ++i) { + if (!isOrientedCcw(pts[prevIdx[i]], pts[i], pts[nextIdx[i]])) { + concaveVertices.push(pts[i]); + } + } + + const triangles = []; + let nVertices = pts.length; + let current = 1; + let skipped = 0; + let next; + let prev; + while (nVertices > 3) { + next = nextIdx[current]; + prev = prevIdx[current]; + const a = pts[prev]; + const b = pts[current]; + const c = pts[next]; + if (isEar(a, b, c, concaveVertices)) { + triangles.push([a, b, c]); + nextIdx[prev] = next; + prevIdx[next] = prev; + concaveVertices.splice(concaveVertices.indexOf(b), 1); + --nVertices; + skipped = 0; + } else if (++skipped > nVertices) { + return []; + } + current = next; + } + next = nextIdx[current]; + prev = prevIdx[current]; + triangles.push([pts[prev], pts[current], pts[next]]); + + return triangles; +} + +export function isConvex(pts: Vec2[]) +{ + if (pts.length < 3) + return false; + + // a polygon is convex if all corners turn in the same direction + // turning direction can be determined using the cross-product of + // the forward difference vectors + let i = pts.length - 2 + let j = pts.length - 1 + let k = 0; + let p = pts[j].sub(pts[i]); + let q = pts[k].sub(pts[j]); + let winding = p.cross(q); + + while (k+1 < pts.length) + { + i = j; + j = k; + k++; + p = pts[j].sub(pts[i]); + q = pts[k].sub(pts[j]); + + if (p.cross(q) * winding < 0) { + return false; + } + } + return true; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 70579230..2223f3d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2078,6 +2078,8 @@ export interface KaboomCtx { * Check if a circle and polygon intersect linewise. */ testCirclePolygon(c: Circle, p: Polygon): boolean; + isConvex(pts:Vec2[]): boolean; + triangulate(pts:Vec2[]): Vec2[][]; Line: typeof Line; Rect: typeof Rect; Circle: typeof Circle; @@ -3865,6 +3867,12 @@ export type DrawPolygonOpt = RenderProps & { * @since v3001.0 */ tex?: Texture; + /** + * Triangulate concave polygons. + * + * @since v3001.0 + */ + triangulate?: boolean }; export interface Outline { @@ -5065,13 +5073,13 @@ export interface PolygonComp extends Comp { colors?: Color[]; /** * The uv of each vertex. - * + * * @since v3001.0 */ uv?: Vec2[]; /** * The texture used when uv coordinates are present. - * + * * @since v3001.0 */ tex?: Texture;