feat: layer hull masks
This commit is contained in:
parent
df61a43ab5
commit
8122018222
|
@ -1,10 +1,14 @@
|
|||
import {simplex2D} from '@leodeslf/simplex-noise';
|
||||
import {Texture} from '@pixi/core';
|
||||
import {Container} from '@pixi/react';
|
||||
import {Sprite} from '@pixi/sprite';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import {usePacket} from '@/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {bresenham, TAU} from '@/util/math.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TargetingGhost from './targeting-ghost.jsx';
|
||||
|
@ -28,12 +32,64 @@ function calculateDarkness(hour) {
|
|||
return Math.floor(darkness * 1000) / 1000;
|
||||
}
|
||||
|
||||
function createLayerMask(layer) {
|
||||
const {area, hulls, tileSize} = layer;
|
||||
const canvas = window.document.createElement('canvas');
|
||||
if (0 === hulls.length) {
|
||||
return Texture.from(canvas);
|
||||
}
|
||||
[canvas.width, canvas.height] = [area.x * tileSize.x, area.y * tileSize.y];
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||||
for (let i = 0; i < hulls.length; ++i) {
|
||||
const {x, y} = hulls[i][0];
|
||||
if (7 !== layer.tile({x: Math.floor(x / tileSize.x), y: Math.floor(y / tileSize.y)})) {
|
||||
continue;
|
||||
}
|
||||
const hull = [...hulls[i]];
|
||||
hull.push(hull[0]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(hull[0].x, hull[0].y);
|
||||
for (let j = 0; j < hull.length - 1; j++) {
|
||||
const p0 = hull[j + 0];
|
||||
const p1 = hull[j + 1];
|
||||
const points = bresenham(p0, p1);
|
||||
const isReversed = (
|
||||
(p0.x > p1.x && points[0].x < points[points.length - 1].x)
|
||||
|| (p0.y > p1.y && points[0].y < points[points.length - 1].y)
|
||||
);
|
||||
for (
|
||||
let k = (isReversed ? points.length - 1 : 0);
|
||||
(isReversed ? k >= 0 : k < points.length);
|
||||
k += (isReversed ? -1 : 1)
|
||||
) {
|
||||
const {x, y} = points[k];
|
||||
const ANGLE_SCALE = 1000;
|
||||
const WALK_SCALE = 10;
|
||||
const MAGNITUDE = ((tileSize.x + tileSize.y) / 2) * simplex2D(x * 100, y * 100);
|
||||
const w = simplex2D(x * WALK_SCALE, y * WALK_SCALE);
|
||||
const r = TAU * simplex2D(x * ANGLE_SCALE, y * ANGLE_SCALE)
|
||||
ctx.lineTo(
|
||||
x + Math.cos(r) * w * MAGNITUDE,
|
||||
y + -Math.sin(r) * w * MAGNITUDE,
|
||||
);
|
||||
}
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
return Texture.from(canvas);
|
||||
}
|
||||
|
||||
export default function Ecs({scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [hour, setHour] = useState(10);
|
||||
const [night, setNight] = useState();
|
||||
const [mask, setMask] = useState();
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
|
@ -79,6 +135,12 @@ export default function Ecs({scale}) {
|
|||
if (update.Time) {
|
||||
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
|
||||
}
|
||||
if (update.TileLayers) {
|
||||
const layer1 = ecs.get(1).TileLayers.layer(1);
|
||||
if (layer1) {
|
||||
setMask(new Sprite(createLayerMask(layer1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedEntities[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
|
@ -101,7 +163,9 @@ export default function Ecs({scale}) {
|
|||
const {Direction, Position, Wielder} = entity;
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
|
||||
const {Camera} = entity;
|
||||
const {TileLayers: {layers: [layer]}, Water: WaterEcs} = ecs.get(1);
|
||||
const {TileLayers, Water: WaterEcs} = ecs.get(1);
|
||||
const layer0 = TileLayers.layer(0);
|
||||
const layer1 = TileLayers.layer(1);
|
||||
const filters = [];
|
||||
if (night) {
|
||||
filters.push(night);
|
||||
|
@ -121,15 +185,26 @@ export default function Ecs({scale}) {
|
|||
>
|
||||
<TileLayer
|
||||
filters={filters}
|
||||
tileLayer={layer}
|
||||
tileLayer={layer0}
|
||||
/>
|
||||
{layer1 && (
|
||||
<TileLayer
|
||||
mask={mask}
|
||||
filters={filters}
|
||||
tileLayer={layer1}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
{WaterEcs && (
|
||||
<Water tileLayer={layer} water={WaterEcs.water} />
|
||||
<Water
|
||||
mask={mask}
|
||||
tileLayer={layer0}
|
||||
water={WaterEcs.water}
|
||||
/>
|
||||
)}
|
||||
{projected && (
|
||||
<TargetingGrid
|
||||
tileLayer={layer}
|
||||
tileLayer={layer0}
|
||||
x={Position.x}
|
||||
y={Position.y}
|
||||
/>
|
||||
|
@ -141,7 +216,7 @@ export default function Ecs({scale}) {
|
|||
{projected?.length > 0 && (
|
||||
<TargetingGhost
|
||||
projected={projected}
|
||||
tileLayer={layer}
|
||||
tileLayer={layer0}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {Container} from '@pixi/display';
|
||||
import {PixiComponent} from '@pixi/react';
|
||||
import '@pixi/spritesheet'; // NECESSARY!
|
||||
import {CompositeTilemap} from '@pixi/tilemap';
|
||||
|
@ -5,14 +6,27 @@ import {CompositeTilemap} from '@pixi/tilemap';
|
|||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||
create: () => new CompositeTilemap(),
|
||||
applyProps: (tilemap, {tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, tileLayer} = props;
|
||||
create: () => {
|
||||
const container = new Container();
|
||||
container.addChild(new CompositeTilemap());
|
||||
return container;
|
||||
},
|
||||
applyProps: (container, {mask: oldMask, tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, mask, tileLayer} = props;
|
||||
const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length);
|
||||
const {textures} = asset;
|
||||
if (tileLayer === oldTileLayer) {
|
||||
return;
|
||||
}
|
||||
if (oldMask) {
|
||||
container.removeChildAt(1);
|
||||
container.mask = undefined;
|
||||
}
|
||||
if (mask) {
|
||||
container.addChild(mask);
|
||||
container.mask = mask;
|
||||
}
|
||||
const tilemap = container.children[0];
|
||||
tilemap.clear();
|
||||
let i = 0;
|
||||
for (let y = 0; y < tileLayer.area.y; ++y) {
|
||||
|
@ -24,15 +38,18 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
|||
})
|
||||
|
||||
export default function TileLayer(props) {
|
||||
const {tileLayer} = props;
|
||||
const {mask, tileLayer} = props;
|
||||
const asset = useAsset(tileLayer.source);
|
||||
if (!asset) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<TileLayerInternal
|
||||
{...props}
|
||||
asset={asset}
|
||||
/>
|
||||
<>
|
||||
<TileLayerInternal
|
||||
{...props}
|
||||
asset={asset}
|
||||
mask={mask}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Graphics} from '@pixi/react';
|
||||
import {Container, Graphics} from '@pixi/react';
|
||||
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
||||
|
@ -15,7 +15,7 @@ const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
|||
return <Graphics alpha={0} draw={draw} ref={ref} />
|
||||
});
|
||||
|
||||
export default function Water({tileLayer, water}) {
|
||||
export default function Water({mask, tileLayer, water}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
@ -39,13 +39,13 @@ export default function Water({tileLayer, water}) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Container mask={mask}>
|
||||
<WaterTile
|
||||
height={tileLayer.tileSize.y}
|
||||
ref={waterTile}
|
||||
width={tileLayer.tileSize.x}
|
||||
/>
|
||||
{waterTiles}
|
||||
</>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
185
app/util/math.js
185
app/util/math.js
|
@ -42,6 +42,70 @@ export const {
|
|||
SQRT2,
|
||||
} = Math;
|
||||
|
||||
export const TAU = Math.PI * 2;
|
||||
|
||||
export function bresenham({x: x1, y: y1}, {x: x2, y: y2}) {
|
||||
const points = [];
|
||||
let x; let y; let px; let py; let xe; let ye;
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const dx1 = Math.abs(dx);
|
||||
const dy1 = Math.abs(dy);
|
||||
px = 2 * dy1 - dx1;
|
||||
py = 2 * dx1 - dy1;
|
||||
if (dy1 <= dx1) {
|
||||
if (dx >= 0) {
|
||||
x = x1; y = y1; xe = x2;
|
||||
}
|
||||
else {
|
||||
x = x2; y = y2; xe = x1;
|
||||
}
|
||||
points.push({x, y});
|
||||
for (let i = 0; x < xe; i++) {
|
||||
x += 1;
|
||||
if (px < 0) {
|
||||
px += 2 * dy1;
|
||||
}
|
||||
else {
|
||||
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
|
||||
y += 1;
|
||||
}
|
||||
else {
|
||||
y -= 1;
|
||||
}
|
||||
px += 2 * (dy1 - dx1);
|
||||
}
|
||||
points.push({x, y});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (dy >= 0) {
|
||||
x = x1; y = y1; ye = y2;
|
||||
}
|
||||
else {
|
||||
x = x2; y = y2; ye = y1;
|
||||
}
|
||||
points.push({x, y});
|
||||
for (let i = 0; y < ye; i++) {
|
||||
y += 1;
|
||||
if (py <= 0) {
|
||||
py += 2 * dx1;
|
||||
}
|
||||
else {
|
||||
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
|
||||
x += 1;
|
||||
}
|
||||
else {
|
||||
x -= 1;
|
||||
}
|
||||
py += 2 * (dx1 - dy1);
|
||||
}
|
||||
points.push({x, y});
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
export function clamp(n, min, max) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
@ -52,6 +116,57 @@ export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
|
|||
return Math.sqrt(xd * xd + yd * yd);
|
||||
}
|
||||
|
||||
export function floodwalk2D(eligible, data, {x, y, w, h}, {diagonal = false} = {}) {
|
||||
const n = data.length;
|
||||
let i = x + w * y;
|
||||
const points = new Set();
|
||||
if (i < 0 || i >= n) {
|
||||
return points;
|
||||
}
|
||||
const seen = [];
|
||||
seen[-1] = true;
|
||||
seen[i] = true;
|
||||
const todo = [i];
|
||||
while (todo.length > 0) {
|
||||
i = todo.pop();
|
||||
const v = data[i];
|
||||
if (!eligible.has(v)) {
|
||||
continue;
|
||||
}
|
||||
points.add(i);
|
||||
const xx = i % w;
|
||||
const yy = Math.floor(i / w);
|
||||
const us = yy >= 1;
|
||||
const rs = xx + 1 < w;
|
||||
const ds = yy + 1 < h;
|
||||
const ls = xx >= 1;
|
||||
const sel = [
|
||||
us ? i - w : -1,
|
||||
rs ? i + 1 : -1,
|
||||
ds ? i + w : -1,
|
||||
ls ? i - 1 : -1,
|
||||
...(
|
||||
diagonal
|
||||
? [
|
||||
us && rs ? i - w + 1 : -1,
|
||||
rs && ds ? i + w + 1 : -1,
|
||||
ds && ls ? i + w - 1 : -1,
|
||||
ls && us ? i - w - 1 : -1,
|
||||
]
|
||||
: []
|
||||
)
|
||||
];
|
||||
for (let i = 0; i < sel.length; ++i) {
|
||||
const j = sel[i];
|
||||
if (!seen[j]) {
|
||||
todo.push(j);
|
||||
seen[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
export function intersects(l, r) {
|
||||
if (l.x0 > r.x1) return false;
|
||||
if (l.y0 > r.y1) return false;
|
||||
|
@ -71,3 +186,73 @@ export function normalizeVector({x, y}) {
|
|||
export function random() {
|
||||
return Math.random();
|
||||
}
|
||||
|
||||
export function isCollinear({x: ax, y: ay}, {x: bx, y: by}, {x: cx, y: cy}) {
|
||||
return (ay - by) * (ax - cx) === (ay - cy) * (ax - bx);
|
||||
}
|
||||
|
||||
export const directionToVector = [
|
||||
{x: 0, y: -1},
|
||||
{x: 1, y: 0},
|
||||
{x: 0, y: 1},
|
||||
{x: -1, y: 0},
|
||||
];
|
||||
|
||||
export function ortho(points, k = {x: 1, y: 1}) {
|
||||
if (points.length < 4) {
|
||||
throw new TypeError('Math.ortho(): points.length < 4');
|
||||
}
|
||||
const index = Object.create(null);
|
||||
for (const i in points) {
|
||||
const point = points[i];
|
||||
if (!index[point.y]) {
|
||||
index[point.y] = Object.create(null);
|
||||
}
|
||||
index[point.y][point.x] = i;
|
||||
}
|
||||
const sorted = [points[0]];
|
||||
let direction = 0;
|
||||
let walk = 0;
|
||||
navigate:
|
||||
while (true) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// ccw rotation
|
||||
const nextDirection = (i + direction + 3) & 3;
|
||||
const {x: px, y: py} = points[walk];
|
||||
const {x: dx, y: dy} = directionToVector[nextDirection];
|
||||
const nextIndex = index[py + k.y * dy]?.[px + k.x * dx];
|
||||
const nextPoint = points[nextIndex];
|
||||
// loop = done
|
||||
if (points[0] === nextPoint) {
|
||||
return sorted;
|
||||
}
|
||||
if (nextPoint) {
|
||||
direction = nextDirection;
|
||||
sorted.push(nextPoint);
|
||||
walk = nextIndex;
|
||||
continue navigate;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function removeCollinear([...vertices]) {
|
||||
if (vertices.length < 3) {
|
||||
return vertices;
|
||||
}
|
||||
vertices.push(vertices[0]);
|
||||
vertices.push(vertices[1]);
|
||||
const trimmed = [];
|
||||
let i = 0;
|
||||
while (i < vertices.length - 2) {
|
||||
if (isCollinear(vertices[i], vertices[i + 1], vertices[i + 2])) {
|
||||
vertices.splice(i + 1, 1);
|
||||
}
|
||||
else {
|
||||
trimmed.push(vertices[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -6,6 +6,7 @@
|
|||
"": {
|
||||
"name": "silphius-next",
|
||||
"dependencies": {
|
||||
"@leodeslf/simplex-noise": "^1.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
|
@ -3018,6 +3019,11 @@
|
|||
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@leodeslf/simplex-noise": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@leodeslf/simplex-noise/-/simplex-noise-1.0.0.tgz",
|
||||
"integrity": "sha512-hsp+CfDnT9jxUjDUqiV2+eLkkdAKz6szlpcyMyXvrgs+LO8a0Gepyri1V+c6FbOfqc3ReWB282GtIE8y3Idi1A=="
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"test": "vitest app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@leodeslf/simplex-noise": "^1.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
|
|
Loading…
Reference in New Issue
Block a user