flow: tiles editing

This commit is contained in:
cha0s 2022-04-03 09:17:08 -05:00
parent 34ba41070d
commit 14cabec9b9
9 changed files with 384 additions and 12 deletions

View File

@ -39,9 +39,29 @@ const Stage = forwardRef(({
'mouseout',
'mousedown',
'mouseup',
'touchend',
'touchmove',
'touchstart',
'wheel',
]
.map((event) => K.fromEvents(renderer.element, event)),
.map((event) => (
K.fromEvents(
renderer.element,
event,
// Normalize touch events.
(event) => {
if (-1 !== ['touchstart', 'touchmove'].indexOf(event.type)) {
/* eslint-disable no-param-reassign */
event.clientX = event.touches[0].clientX;
event.deltaX = event.touches[0].deltaX;
event.clientY = event.touches[0].clientY;
event.deltaY = event.touches[0].deltaY;
/* eslint-enable no-param-reassign */
}
return event;
},
)
)),
),
}));
useEffect(() => {

View File

@ -34,7 +34,7 @@ const makeButtonSelector = (key) => (
);
const modes = [
'move',
'pan',
'edit',
'sample',
]
@ -130,6 +130,7 @@ const RoomComponent = ({
active: true,
events: 0 !== mode && events,
room,
roomRenderable,
resource,
},
)

View File

@ -14,10 +14,8 @@
.side, .stage {
height: 50%;
}
.side {
overflow: auto;
}
.stage {
margin: 1rem 0 0 1rem;
overflow: hidden;
}
@media (min-width: 60rem) {
@ -29,7 +27,7 @@
}
}
.stage.moving {
.stage.moving > div {
overflow: auto;
}
@ -74,7 +72,7 @@
margin-top: 0.5rem;
}
label.move {
label.pan {
font-size: 0.5rem;
button {
mask-image: url('./img/scan-light.svg');

View File

@ -90,18 +90,20 @@ function EntitiesPage({
);
};
switch (type) {
case 'mousedown': {
case 'mousedown':
case 'touchstart': {
updatePosition();
setIsHolding(true);
break;
}
case 'touchmove':
case 'mousemove': {
if (isHolding) {
updatePosition();
}
break;
}
case 'mouseout':
case 'touchend':
case 'mouseup': {
setIsHolding(false);
break;

View File

@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
:global(.entity-renderer) {
flex-grow: 1;

View File

@ -1,12 +1,261 @@
import {
Color,
Container,
Primitives,
Renderer,
Sprite,
Stage,
} from '@avocado/graphics';
import {
Rectangle,
Vector,
} from '@avocado/math';
import {
classnames,
hot,
PropTypes,
React,
useCallback,
useEffect,
useState,
} from '@flecks/react';
function TilesPage() {
import styles from './page.module.scss';
const container = new Container();
const primitives = new Primitives();
const renderer = new Renderer();
const calculateVisibility = (mute, solo) => {
const visibility = Array(mute.length).fill(true);
if (solo.some((is) => !!is)) {
for (let i = 0; i < solo.length; i++) {
visibility[i] = solo[i];
}
}
for (let i = 0; i < mute.length; i++) {
if (mute[i]) {
visibility[i] = false;
}
}
return visibility;
};
function TilesPage({
events,
resource,
room,
roomRenderable,
}) {
const layerCount = resource.tiles.length;
const [currentLayer, setCurrentLayer] = useState(0);
const [tilesetEvents, setTilesetEvents] = useState();
const [solo, setSolo] = useState(Array(layerCount).fill(false));
const [incident, setIncident] = useState([0, 0]);
const [isHolding, setIsHolding] = useState(false);
const [mute, setMute] = useState(Array(layerCount).fill(false));
const [selection, setSelection] = useState([0, 0, 16, 16]);
// const [sprite, setSprite] = useState(null);
const [tilesetMode, setTilesetMode] = useState(0);
const [viewport, setViewport] = useState([0, 0]);
const onRef = useCallback((stage) => {
setTilesetEvents(0 !== tilesetMode && stage?.events());
}, [setTilesetEvents, tilesetMode]);
useEffect(() => {
const visibility = calculateVisibility(mute, solo);
for (let i = 0; i < visibility.length; i++) {
// eslint-disable-next-line no-param-reassign
roomRenderable.tilesRenderables[i].alpha = visibility[i] ? 1 : 0;
}
}, [mute, roomRenderable, solo]);
useEffect(() => {
const {image} = room.tiles[currentLayer].atlas;
container.removeAllChildren();
const sprite = new Sprite(image);
sprite.anchor = [0, 0];
container.addChild(sprite);
container.addChild(primitives);
setViewport(image.size);
}, [currentLayer, room]);
useEffect(() => {
primitives.anchor = [0, 0];
primitives.clear();
primitives.drawRectangle(
selection,
Primitives.lineStyle(new Color(255, 0, 255, 0.2), 0),
Primitives.fillStyle(new Color(255, 0, 255, 0.2)),
);
}, [selection]);
useEffect(() => {
if (!events) {
return undefined;
}
const onValue = (
// {
// clientX,
// clientY,
// deltaY,
// target,
// type,
// },
) => {
};
events.onValue(onValue);
return () => {
events.offValue(onValue);
};
});
useEffect(() => {
if (!tilesetEvents) {
return undefined;
}
const onValue = (
{
clientX,
clientY,
target,
type,
},
) => {
const tileSize = [16, 16];
switch (type) {
case 'touchstart':
case 'mousedown': {
const {left, top} = target.getBoundingClientRect();
const clamped = Vector.mul(
tileSize,
Vector.floor(Vector.div([clientX - left, clientY - top], tileSize)),
);
setIncident(clamped);
setSelection(Rectangle.compose(clamped, tileSize));
setIsHolding(true);
break;
}
case 'touchmove':
case 'mousemove': {
if (isHolding) {
const {left, top} = target.getBoundingClientRect();
const tileSize = [16, 16];
const clamped = Vector.mul(
tileSize,
Vector.floor(Vector.div([clientX - left, clientY - top], tileSize)),
);
const min = Vector.min(clamped, incident);
const max = Vector.max(clamped, incident);
setSelection(Rectangle.compose(min, Vector.add(tileSize, Vector.sub(max, min))));
}
break;
}
case 'touchend':
case 'mouseup': {
setIsHolding(false);
break;
}
default:
}
};
tilesetEvents.onValue(onValue);
return () => {
tilesetEvents.offValue(onValue);
};
});
return (
<div />
<div className={classnames(styles.tilesPage, {[styles.holding]: isHolding})}>
<div className={classnames('label', styles.label)}>
<div>Tiles</div>
<ul>
{
resource.tiles.map((tiles, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i}>
<button
className={styles.layerButton}
onClick={(event) => {
// Bug? .stopPropagation() doesn't work when clicking the child checkboxes.
if (event.target === event.currentTarget) {
setCurrentLayer(i);
}
}}
type="button"
>
<div className={styles.info}>
{i}
{' '}
-
{' '}
{tiles.tileImageUri}
</div>
<div className={styles.visibility}>
<label>
S
<input
checked={solo[i]}
onChange={(event) => {
event.stopPropagation();
const newSolo = solo.concat();
newSolo[i] = !newSolo[i];
setSolo(newSolo);
}}
type="checkbox"
/>
</label>
<label>
M
<input
checked={mute[i]}
onChange={(event) => {
event.stopPropagation();
const newMute = mute.concat();
newMute[i] = !newMute[i];
setMute(newMute);
}}
type="checkbox"
/>
</label>
</div>
</button>
</li>
))
}
</ul>
</div>
<div>
<div className={styles.draw}>
<div className={styles.brushes}>
<label>
<span>Tileset mode</span>
<select
defaultValue={tilesetMode}
onChange={(event) => {
setTilesetMode(parseInt(event.target.value, 10));
}}
>
<option value={0}>Pan</option>
<option value={1}>Draw</option>
<option value={2}>Fill</option>
</select>
</label>
</div>
<div className={styles.tilesetWrapper}>
<div
className={classnames(styles.tileset, {[styles.moving]: tilesetMode === 0})}
>
<Stage
centered={false}
ref={onRef}
renderer={renderer}
renderable={container}
scalable={false}
size={viewport}
/>
</div>
</div>
</div>
<div className={styles.tilesetUri}>
{resource.tiles[currentLayer].tileImageUri}
</div>
</div>
</div>
);
}
@ -15,7 +264,14 @@ TilesPage.defaultProps = {};
TilesPage.displayName = 'TilesPage';
TilesPage.propTypes = {
room: PropTypes.shape({}).isRequired,
// eslint-disable-next-line react/forbid-prop-types
events: PropTypes.any.isRequired,
// eslint-disable-next-line react/forbid-prop-types
room: PropTypes.any.isRequired,
// eslint-disable-next-line react/forbid-prop-types
roomRenderable: PropTypes.any.isRequired,
// eslint-disable-next-line react/forbid-prop-types
resource: PropTypes.any.isRequired,
};
export default hot(module)(TilesPage);

View File

@ -0,0 +1,90 @@
.tilesPage {
height: 100%;
overflow: auto;
&.holding {
overflow: hidden;
}
hr {
border-color: rgba(255, 255, 255, 0.1);
margin: 0.5rem;
}
.label, label {
background-color: transparent;
}
}
.layerButton {
align-items: baseline;
display: flex;
flex-wrap: wrap;
font-family: var(--system-font-family);
gap: 0.5rem;
justify-content: space-between;
margin: 0.5rem 0 0 0;
width: 100%;
.info {
padding-right: 0.5rem;
}
label {
background-color: transparent;
display: inline;
// padding: 0.5rem 0 0;
}
.visibility {
display: flex;
flex-wrap: nowrap;
gap: 1rem;
label {
display: flex;
flex-direction: row;
min-height: auto;
padding: 0;
}
}
}
.tilesetUri {
margin: 1rem;
}
.draw {
// display: flex;
// flex-wrap: wrap;
}
.tilesetWrapper {
margin: 1rem;
}
.tileset {
aspect-ratio: 1;
background-image: url('./test.png');
display: flex;
flex-grow: 100;
margin: 0 auto;
max-width: 20rem;
overflow: hidden;
&.moving {
overflow: auto;
}
}
.tilesetBar {
align-items: center;
background-color: #333;
display: flex;
height: 3rem;
width: 100%;
}
.brushes label span {
font-family: var(--system-font-family);
font-size: 0.6em;
font-weight: bold;
margin-bottom: 1em;
// text-align: center;
// text-orientation: mixed;
text-transform: uppercase;
// writing-mode: vertical-rl;
// width: 1rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -61,6 +61,10 @@ export default (flecks) => {
}
}
get atlas() {
return this.$$atlas;
}
cleanPackets() {
this.$$packets = [];
this.$$updates = [];