Skip to content

Commit

Permalink
Add wrapped mode support (#77)
Browse files Browse the repository at this point in the history
- split snake body in segments
- add portals for wrapped body segments
- stick firstPoint and lastPoint to the border
- code refactoring
  • Loading branch information
und3f authored Jan 20, 2024
1 parent babfa62 commit d47d6dd
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 149 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

339 changes: 195 additions & 144 deletions src/lib/components/SvgSnakeBody.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<script lang="ts">
import type { Point, Snake } from "$lib/playback/types";
import { type SvgCalcParams, type SvgPoint, svgCalcCellCenter } from "$lib/svg";
import {
type SvgCalcParams,
type SvgPoint,
type SvgCircleProps,
svgCalcCellCenter
} from "$lib/svg";
import { calcSourceWrapPosition, isAdjacentPoint, isEqualPoint } from "$lib/geometry";
type SvgPointWithCircleProps = SvgPoint & SvgCircleProps;
// Used to very slightly extend body segments to ensure overlap with head and tail
const OVERLAP = 0.1;
Expand All @@ -9,170 +16,212 @@
export let svgCalcParams: SvgCalcParams;
// Calculate the center points of a line that paths along snake.body
$: bodyPolylinePoints = calcBodyPolylinePoints(snake);
function calcBodyPolylinePoints(snake: Snake): SvgPoint[] {
$: bodyPolylinesPoints = calcBodyPolylinesPoints(snake);
function calcBodyPolylinesPoints(snake: Snake): SvgPoint[][] {
// Make a copy of snake body and separate into head, tail, and body.
const body: Point[] = [...snake.body];
const head = body.shift() as Point;
const tail = body.pop() as Point;
// Ignore body parts that are stacked on the tail
// This ensures that the tail is always shown even when the snake has grown
while (
body.length > 0 &&
body[body.length - 1].x == tail.x &&
body[body.length - 1].y == tail.y
) {
for (let last = body.at(-1); isEqualPoint(last, tail); last = body.at(-1)) {
body.pop();
}
if (body.length == 0) {
// If we're drawing no body, but head and tail are different,
// they still need to be connected.
if (!isEqualPoint(head, tail)) {
const svgCenter = svgCalcCellCenter(svgCalcParams, head);
return [calcHeadToTailJoint(head, tail, svgCenter)];
}
return [[]];
}
return convertBodyToPolilines(body, head, tail);
}
function convertBodyToPolilines(body: Point[], head: Point, tail: Point): SvgPoint[][] {
const gapSize = svgCalcParams.cellSpacing + OVERLAP;
// Split wrapped body parts into separated segments
const bodySegments = splitBodySegments(body);
// Get the center point of each body square we're going to render
const bodyCenterPoints = body.map((p) => {
const svgCenter = svgCalcCellCenter(svgCalcParams, p);
return { cx: svgCenter.x, cy: svgCenter.y, x: p.x, y: p.y };
});
// If we're drawing *any* body, we want to extend the first and last points
// to connect to the head and tail across the cell spacing.
if (bodyCenterPoints.length > 0) {
// Use overlap to ensure that we connect to head and tail.
const gapSize = svgCalcParams.cellSpacing + OVERLAP;
// Extend first point towards head
const first = bodyCenterPoints[0];
if (head.x > first.x) {
bodyCenterPoints.unshift({
cx: first.cx + svgCalcParams.cellSizeHalf + gapSize,
cy: first.cy,
x: 0,
y: 0
});
} else if (head.x < first.x) {
bodyCenterPoints.unshift({
cx: first.cx - svgCalcParams.cellSizeHalf - gapSize,
cy: first.cy,
x: 0,
y: 0
});
} else if (head.y > first.y) {
bodyCenterPoints.unshift({
cx: first.cx,
cy: first.cy - svgCalcParams.cellSizeHalf - gapSize,
x: 0,
y: 0
});
} else if (head.y < first.y) {
bodyCenterPoints.unshift({
cx: first.cx,
cy: first.cy + svgCalcParams.cellSizeHalf + gapSize,
x: 0,
y: 0
});
const bodySegmentsCenterPoints = bodySegments.map((segment) =>
segment.map(enrichSvgCellCenter)
);
// Extend each wrapped segment towards border
for (let i = 0; i < bodySegmentsCenterPoints.length; i++) {
// Extend each segment last point towards border
if (i < bodySegmentsCenterPoints.length - 1) {
const cur = bodySegmentsCenterPoints[i].at(-1) as SvgPointWithCircleProps;
const next = bodySegmentsCenterPoints[i + 1][0];
bodySegmentsCenterPoints[i].push(calcBorderJoint(cur, next));
}
// Extend last point towards tail
const last = bodyCenterPoints[bodyCenterPoints.length - 1];
if (tail.x > last.x) {
bodyCenterPoints.push({
cx: last.cx + svgCalcParams.cellSizeHalf + gapSize,
cy: last.cy,
x: 0,
y: 0
});
} else if (tail.x < last.x) {
bodyCenterPoints.push({
cx: last.cx - svgCalcParams.cellSizeHalf - gapSize,
cy: last.cy,
x: 0,
y: 0
});
} else if (tail.y > last.y) {
bodyCenterPoints.push({
cx: last.cx,
cy: last.cy - svgCalcParams.cellSizeHalf - gapSize,
x: 0,
y: 0
});
} else if (tail.y < last.y) {
bodyCenterPoints.push({
cx: last.cx,
cy: last.cy + svgCalcParams.cellSizeHalf + gapSize,
x: 0,
y: 0
});
// Extend segment's first point toward border portal
if (i > 0) {
const cur = bodySegmentsCenterPoints[i][0];
const prev = bodySegmentsCenterPoints[i - 1].at(-1) as Point;
bodySegmentsCenterPoints[i].unshift(calcBorderJoint(cur, prev));
}
}
// If we're drawing no body, but head and tail are different,
// they still need to be connected.
if (bodyCenterPoints.length == 0) {
if (head.x != tail.x || head.y != tail.y) {
const svgCenter = svgCalcCellCenter(svgCalcParams, head);
if (head.x > tail.x) {
bodyCenterPoints.push({
cx: svgCenter.x - svgCalcParams.cellSizeHalf + OVERLAP,
cy: svgCenter.y,
x: 0,
y: 0
});
bodyCenterPoints.push({
cx: svgCenter.x - svgCalcParams.cellSizeHalf - svgCalcParams.cellSpacing - OVERLAP,
cy: svgCenter.y,
x: 0,
y: 0
});
} else if (head.x < tail.x) {
bodyCenterPoints.push({
cx: svgCenter.x + svgCalcParams.cellSizeHalf - OVERLAP,
cy: svgCenter.y,
x: 0,
y: 0
});
bodyCenterPoints.push({
cx: svgCenter.x + svgCalcParams.cellSizeHalf + svgCalcParams.cellSpacing + OVERLAP,
cy: svgCenter.y,
x: 0,
y: 0
});
} else if (head.y > tail.y) {
bodyCenterPoints.push({
cx: svgCenter.x,
cy: svgCenter.y + svgCalcParams.cellSizeHalf - OVERLAP,
x: 0,
y: 0
});
bodyCenterPoints.push({
cx: svgCenter.x,
cy: svgCenter.y + svgCalcParams.cellSizeHalf + svgCalcParams.cellSpacing + OVERLAP,
x: 0,
y: 0
});
} else if (head.y < tail.y) {
bodyCenterPoints.push({
cx: svgCenter.x,
cy: svgCenter.y - svgCalcParams.cellSizeHalf + OVERLAP,
x: 0,
y: 0
});
bodyCenterPoints.push({
cx: svgCenter.x,
cy: svgCenter.y - svgCalcParams.cellSizeHalf - svgCalcParams.cellSpacing - OVERLAP,
x: 0,
y: 0
});
}
}
// Extend first point towards head
const firstPoint = bodySegmentsCenterPoints[0][0];
if (isAdjacentPoint(head, firstPoint)) {
bodySegmentsCenterPoints[0].unshift(calcJoint(firstPoint, head, gapSize));
} else {
// Add head portal
bodySegmentsCenterPoints[0].unshift(calcBorderJoint(enrichSvgCellCenter(firstPoint), head));
}
// Extend last point towards tail
const lastPoint = bodySegmentsCenterPoints.at(-1)?.at(-1) as SvgPointWithCircleProps;
if (isAdjacentPoint(lastPoint, tail)) {
bodySegmentsCenterPoints.at(-1)?.push(calcJoint(lastPoint, tail, gapSize));
} else {
// Add tail portal
bodySegmentsCenterPoints.at(-1)?.push(calcBorderJoint(lastPoint, tail));
}
// Finally, return an array of SvgPoints to use for a polyline
return bodyCenterPoints.map((obj) => {
return { x: obj.cx, y: obj.cy };
});
return bodySegmentsCenterPoints.map((segment) =>
segment.map((obj) => ({ x: obj.cx, y: obj.cy }))
);
}
function splitBodySegments(body: Point[]): Point[][] {
if (body.length == 0) {
return [[]];
}
let prev = body[0];
const segments: Point[][] = [[prev]];
for (let i = 1; i < body.length; i++) {
const cur = body[i];
// Start new segment
if (!isAdjacentPoint(cur, prev)) {
segments.push([]);
}
segments.at(-1)?.push(cur);
prev = cur;
}
return segments;
}
function enrichSvgCellCenter(p: Point): SvgPointWithCircleProps {
const c = svgCalcCellCenter(svgCalcParams, p);
return {
cx: c.x,
cy: c.y,
...p
};
}
function calcBorderJoint(src: SvgPointWithCircleProps, dst: Point): SvgPointWithCircleProps {
const border = calcSourceWrapPosition(src, dst);
return calcJoint(src, border);
}
function calcJoint(
src: SvgPointWithCircleProps,
dst: Point,
gapSize = 0
): SvgPointWithCircleProps {
// Extend source point towards destination
if (dst.x > src.x) {
return {
...src,
cx: src.cx + svgCalcParams.cellSizeHalf + gapSize,
cy: src.cy
};
} else if (dst.x < src.x) {
return {
...src,
cx: src.cx - svgCalcParams.cellSizeHalf - gapSize,
cy: src.cy
};
} else if (dst.y > src.y) {
return {
...src,
cx: src.cx,
cy: src.cy - svgCalcParams.cellSizeHalf - gapSize
};
} else if (dst.y < src.y) {
return {
...src,
cx: src.cx,
cy: src.cy + svgCalcParams.cellSizeHalf + gapSize
};
}
// In error cases there could be duplicate point
throw new Error("Same point have no joint.");
}
$: drawBody = bodyPolylinePoints.length > 0;
function calcHeadToTailJoint(head: Point, tail: Point, svgCenter: Point): SvgPoint[] {
if (head.x > tail.x) {
return [
{
x: svgCenter.x - svgCalcParams.cellSizeHalf + OVERLAP,
y: svgCenter.y
},
{
x: svgCenter.x - svgCalcParams.cellSizeHalf - svgCalcParams.cellSpacing - OVERLAP,
y: svgCenter.y
}
];
} else if (head.x < tail.x) {
return [
{
x: svgCenter.x + svgCalcParams.cellSizeHalf - OVERLAP,
y: svgCenter.y
},
{
x: svgCenter.x + svgCalcParams.cellSizeHalf + svgCalcParams.cellSpacing + OVERLAP,
y: svgCenter.y
}
];
} else if (head.y > tail.y) {
return [
{
x: svgCenter.x,
y: svgCenter.y + svgCalcParams.cellSizeHalf - OVERLAP
},
{
x: svgCenter.x,
y: svgCenter.y + svgCalcParams.cellSizeHalf + svgCalcParams.cellSpacing + OVERLAP
}
];
} else if (head.y < tail.y) {
return [
{
x: svgCenter.x,
y: svgCenter.y - svgCalcParams.cellSizeHalf + OVERLAP
},
{
x: svgCenter.x,
y: svgCenter.y - svgCalcParams.cellSizeHalf - svgCalcParams.cellSpacing - OVERLAP
}
];
}
throw new Error("Head and tail is a same point.");
}
$: drawBody = bodyPolylinesPoints[0].length > 0;
$: bodyPolylinesProps = bodyPolylinesPoints.map(calcBodyPolylineProps);
$: bodyPolylineProps = calcBodyPolylineProps(bodyPolylinePoints);
function calcBodyPolylineProps(polylinePoints: SvgPoint[]) {
// Convert points into a string of the format "x1,y1 x2,y2, ...
const points = polylinePoints
Expand All @@ -191,5 +240,7 @@
</script>

{#if drawBody}
<polyline stroke={snake.color} fill="transparent" {...bodyPolylineProps} />
{#each bodyPolylinesProps as polylineProps}
<polyline stroke={snake.color} fill="transparent" {...polylineProps} />
{/each}
{/if}
Loading

0 comments on commit d47d6dd

Please sign in to comment.