feat: layer hull masks

This commit is contained in:
cha0s 2024-07-07 17:34:40 -05:00
parent df61a43ab5
commit 8122018222
6 changed files with 301 additions and 17 deletions

View File

@ -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>

View File

@ -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}
/>
</>
);
}

View File

@ -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>
);
}

View File

@ -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
View File

@ -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",

View File

@ -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",