This commit is contained in:
Renan LE CARO 2025-03-13 16:43:00 +01:00
parent 78c8e154c6
commit b0d8827e09
11 changed files with 19670 additions and 797 deletions

View file

@ -0,0 +1,12 @@
import {resizeLevel} from "./levels_editor_util";
test('resizeLevel',()=>{
expect(resizeLevel({
name:'',
bricks:'AAAA',
size:2,
svg:null,
color:''
}, 1)).toBe({bricks:'AA_AA____',size:3});
})

View file

@ -11,10 +11,8 @@
</head>
<body>
<div id="levels"></div>
<div id="palette">
<button id="new-level">new</button>
</div>
<div id="app"></div>
<script type="module" src="./levels_editor.ts"></script>
<script type="module" src="levels_editor.tsx"></script>
</body>

View file

@ -1,270 +0,0 @@
import {Palette, RawLevel} from "./types";
import _backgrounds from './backgrounds.json'
const backgrounds = _backgrounds as string[];
import _palette from './palette.json'
const palette = _palette as Palette;
import _allLevels from './levels.json'
import {getLevelBackground, hashCode} from "./getLevelBackground";
let allLevels = _allLevels as RawLevel[];
let currentCode = '_'
const paletteEl = document.getElementById('palette') as HTMLDivElement;
Object.entries(palette).forEach(([code, color]) => {
const btn = document.createElement('button')
Object.assign(btn.style, {
background: color || 'linear-gradient(45deg,black,white)',
display: 'inline-block',
width: '40px',
height: '40px',
border: '1px solid black'
})
if (code === currentCode) {
btn.className = 'active'
}
paletteEl.appendChild(btn)
btn.addEventListener('click', (e) => {
currentCode = code
e.preventDefault()
document.querySelector('#palette button.active')?.classList.remove('active');
btn.classList.add('active')
})
})
function renderAllLevels() {
allLevels.forEach((level, levelIndex) => {
addLevelEditorToList(level, levelIndex)
})
}
const levelsListEl = document?.getElementById('levels') as HTMLDivElement
function addLevelEditorToList(level: RawLevel, levelIndex: number) {
const {name, bricks, size, svg, color} = level
let div = document.createElement('div')
div.innerHTML = `
<input type="text" value="${level.name || ''}" data-level="${levelIndex}" data-text-val="name" />
<div>
<button data-level="${levelIndex}" data-delete="yep">Delete</button>
<button data-offset-level-size="-1" data-level="${levelIndex}">-</button>
<button data-offset-level-size="1" data-level="${levelIndex}">+</button>
<button data-offset-x="-1" data-offset-y="0" data-level="${levelIndex}">L</button>
<button data-offset-x="1" data-offset-y="0" data-level="${levelIndex}">R</button>
<button data-offset-x="0" data-offset-y="-1" data-level="${levelIndex}">U</button>
<button data-offset-x="0" data-offset-y="1" data-level="${levelIndex}">D</button>
<input type="color" value="${level.color || ''}" data-level="${levelIndex}" data-text-val="color" />
<input type="number" value="${level.svg || (hashCode(level.name) % backgrounds.length)}" data-level="${levelIndex}" data-num-val="svg" />
</div>
<div class="level-bricks-preview" id="bricks-of-${levelIndex}" >
</div>
`;
levelsListEl.appendChild(div)
renderLevelBricks(levelIndex)
updateLevelBackground(levelIndex)
}
function updateLevelBackground(levelIndex: number) {
const div = document.getElementById("bricks-of-" + levelIndex) as HTMLDivElement
const level = allLevels[levelIndex]
const {svg, color} = level
if (color) {
Object.assign(div.style, {backgroundImage: 'none', backgroundColor: color})
} else {
const svgSource = getLevelBackground(level) as string
div.setAttribute('data-svg', svgSource)
Object.assign(div.style, {
backgroundImage: `url("data:image/svg+xml;UTF8,${encodeURIComponent(svgSource)}")`,
backgroundColor: 'transparent'
})
}
}
function renderLevelBricks(levelIndex: number) {
const {size, bricks} = allLevels[levelIndex]
const buttons = []
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const index = y * size + x
buttons.push(`<button style="background: ${palette[bricks[index]] || 'transparent'}; left:${x * 40}px;top:${y * 40
}px;width:40px;height: 40px; position: absolute" data-set-color-of="${index}" data-level="${levelIndex}"></button>`)
}
}
const div = document.getElementById("bricks-of-" + levelIndex) as HTMLDivElement
div.innerHTML = buttons.join('')
Object.assign(div.style, {
width: size * 40 + 'px',
height: size * 40 + 'px'
})
}
levelsListEl.addEventListener('change', e => {
const target = e.target as HTMLInputElement
const levelIndexStr = target.getAttribute('data-level')
if (!levelIndexStr) return
const levelIndex = parseInt(levelIndexStr)
const level = allLevels[levelIndex]
if (target.getAttribute('data-text-val') == 'name') {
level.name = target.value
}
if (target.getAttribute('data-text-val') == 'color') {
level.color = target.value
level.svg = null
}
if (target.getAttribute('data-num-val') == 'svg') {
level.color = ''
level.svg = parseFloat(target.value)
}
updateLevelBackground(levelIndex)
save()
})
levelsListEl.addEventListener('click', e => {
const target = e.target as HTMLButtonElement
if (target.tagName !== 'BUTTON') return
const resize = target.getAttribute('data-offset-level-size')
const moveX = target.getAttribute('data-offset-x')
const moveY = target.getAttribute('data-offset-y')
const levelIndexStr = target.getAttribute('data-level')
if (!levelIndexStr) return
const levelIndex = parseInt(levelIndexStr)
const level = allLevels[levelIndex]
const {bricks, size} = level;
if (resize) {
const newSize = size + parseInt(resize)
const newBricks = new Array(newSize * newSize).fill('_')
for (let x = 0; x < Math.min(size, newSize); x++) {
for (let y = 0; y < Math.min(size, newSize); y++) {
newBricks[y * newSize + x] = bricks.split('')[y * size + x] || '_'
}
}
level.size = newSize;
level.bricks = newBricks.map(b => b || '_').join('');
} else if (moveX && moveY) {
const dx = parseInt(moveX), dy = parseInt(moveY)
const newBricks = new Array(size * size).fill('_')
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
newBricks[(y + dy) * size + (x + dx)] = bricks.split('')[y * size + x] || '_'
}
}
level.bricks = newBricks.map(b => b || '_').join('');
} else if (target.getAttribute('data-rename')) {
const newName = prompt('Name ? ', level.name)
if (newName) {
level.name = newName
target.textContent = newName
}
} else if (target.getAttribute('data-delete')) {
if (confirm('Delete level')) {
allLevels = allLevels.filter((l, i) => i !== levelIndex)
save().then(() => window.location.reload())
}
}
renderLevelBricks(levelIndex)
save()
}, true)
let applying = ''
function colorPixel(e: Event) {
const target = e.target as HTMLButtonElement
if (applying === '') return
console.log('colorPixel', applying)
const index = target.getAttribute('data-set-color-of')
const level = target.getAttribute('data-level')
if (index && level) {
const levelIndex = parseInt(level)
target.style.background = palette[applying] || 'transparent'
setBrick(levelIndex, parseInt(index), applying)
}
}
function setBrick(levelIndex: number, index: number, chr: string) {
const bricks = allLevels[levelIndex].bricks
allLevels[levelIndex].bricks = bricks.substring(0, index) + chr + bricks.substring(index + 1);
}
let changed = 0
levelsListEl.addEventListener('mousedown', e => {
const target = e.target as HTMLButtonElement
const index = target.getAttribute('data-set-color-of')
const level = target.getAttribute('data-level')
if (index && level) {
changed = 0
const before = allLevels[parseInt(level)].bricks[parseInt(index)] || ''
applying = before === currentCode ? '_' : currentCode
console.log({before, applying, currentCode})
colorPixel(e)
}
})
levelsListEl.addEventListener('mouseenter', e => {
if (applying !== '') {
colorPixel(e)
changed++
}
}, true);
document.addEventListener('mouseup', (e: Event) => {
applying = '';
if (changed) {
save()
}
;
});
(document.getElementById('new-level') as HTMLButtonElement).addEventListener('click', (e: Event) => {
const name = prompt("Name ? ")
if (!name) return;
allLevels.push({
name,
size: 8,
bricks: '________________________________________________________________',
svg: null,
color: ''
})
const levelIndex = allLevels.length - 1
addLevelEditorToList(allLevels[levelIndex], levelIndex)
save()
}, true)
renderAllLevels()
function save() {
return fetch('http://localhost:4400/src/levels.json', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: JSON.stringify(allLevels, null, 2)
})
}

148
src/levels_editor.tsx Normal file
View file

@ -0,0 +1,148 @@
import {Palette, RawLevel} from "./types";
import _backgrounds from './backgrounds.json'
import _palette from './palette.json'
import _allLevels from './levels.json'
import {getLevelBackground, hashCode} from "./getLevelBackground";
import {createRoot} from 'react-dom/client';
import {useCallback, useState} from "react";
import {moveLevel, resizeLevel, setBrick} from "./levels_editor_util";
const backgrounds = _backgrounds as string[];
const palette = _palette as Palette;
let allLevels = _allLevels as RawLevel[];
function save() {
return fetch('http://localhost:4400/src/levels.json', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: JSON.stringify(allLevels, null, 2)
})
}
function App() {
const [selected, setSelected] = useState('W')
const [applying, setApplying] = useState('')
const [levels, setLevels] = useState(allLevels)
const updateLevel = useCallback((index: number, change: Partial<RawLevel>) => {
setLevels(list => list.map((l, li) => li === index ? {...l, ...change} : l))
}, []);
const deleteLevel = useCallback((li: number) => {
if (confirm('Delete level')) {
allLevels = allLevels.filter((l, i) => i !== li)
}
}, [])
return <div onMouseUp={() => setApplying('')} onMouseLeave={() => setApplying('')}>
<div id={"levels"}>
{
levels.map((level, li) => {
const {name, bricks, size, svg, color} = level
const brickButtons = []
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const index = y * size + x
brickButtons.push(<button
key={index}
onMouseDown={() => {
const color = selected === bricks[index] ? '_' : applying
setApplying(color)
updateLevel(li, setBrick(level, index, color))
}}
onMouseEnter={() => {
if (applying) {
updateLevel(li, setBrick(level, index, applying))
}
}}
style={{
background: palette[bricks[index]] || 'transparent',
left: x * 40, top: y * 40, width: 40, height: 40, position: 'absolute'
}}></button>)
}
}
const background = color ? {backgroundImage: 'none', backgroundColor: color} : {
backgroundImage: `url("data:image/svg+xml;UTF8,${encodeURIComponent(getLevelBackground(level) as string)}")`,
backgroundColor: 'transparent'
}
return <div key={li}>
<input type="text" value={name} onChange={e => updateLevel(li, {name: e.target.value})}/>
<div>
<button onClick={() => deleteLevel(li)}>Delete</button>
<button onClick={() => updateLevel(li, resizeLevel(level, -1))}>-</button>
<button onClick={() => updateLevel(li, resizeLevel(level, +1))}>+</button>
<button onClick={() => updateLevel(li, moveLevel(level, -1, 0))}>L</button>
<button onClick={() => updateLevel(li, moveLevel(level, 1, 0))}>R</button>
<button onClick={() => updateLevel(li, moveLevel(level, 0, -1))}>U</button>
<button onClick={() => updateLevel(li, moveLevel(level, 0, 1))}>D</button>
<input type="color" value={level.color || ''}
onChange={e => e.target.value && updateLevel(li, {color: e.target.value})}/>
<input type="number" value={level.svg || (hashCode(level.name) % backgrounds.length)}
onChange={e => !isNaN(parseFloat(e.target.value)) && updateLevel(li, {
color: '',
svg: parseFloat(e.target.value)
})}
/>
</div>
<div className="level-bricks-preview" style={{
width: size * 40,
height: size * 40,
...background
}}>
{brickButtons}
</div>
</div>
}
)
}
</div>
<div id={"palette"}>
{
Object.entries(palette).map(([code, color]) => <button
key={code}
className={code === selected ? 'active' : ''}
style={{
background: color || 'linear-gradient(45deg,black,white)',
display: 'inline-block',
width: '40px',
height: '40px',
border: '1px solid black'
}}
onClick={() => setSelected(code)}></button>
)
}
</div>
<button id="new-level" onClick={() => {
const name = prompt("Name ? ")
if (!name) return;
setLevels(l => [...l, {
name,
size: 8,
bricks: '________________________________________________________________',
svg: null,
color: ''
}])
}}>new
</button>
</div>;
}
const root = createRoot(document.getElementById('app') as HTMLDivElement);
root.render(<App/>);

40
src/levels_editor_util.ts Normal file
View file

@ -0,0 +1,40 @@
import {RawLevel} from "./types";
export function resizeLevel(level: RawLevel, sizeDelta: number) {
const {size, bricks} = level
const newSize = Math.max(1, size + sizeDelta)
const newBricks = new Array(newSize * newSize).fill('_')
for (let x = 0; x < Math.min(size, newSize); x++) {
for (let y = 0; y < Math.min(size, newSize); y++) {
newBricks[y * newSize + x] = bricks.split('')[y * size + x] || '_'
}
}
return {
size: newSize,
bricks: newBricks.join('')
}
}
export function moveLevel(level: RawLevel, dx: number, dy: number) {
const {size, bricks} = level
const newBricks = new Array(size * size).fill('_')
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
newBricks[y * size + x] = bricks.split('')[(y - dy) * size + (x - dx)] || '_'
}
}
return {
bricks: newBricks.join('')
}
}
export function setBrick(level: RawLevel, index: number, colorCode: string) {
let bricksString = level.bricks.slice(0, level.size * level.size)
if (bricksString.length < level.size * level.size) {
bricksString += '_'.repeat(level.size * level.size - bricksString.length)
}
const bricks = bricksString.split('')
bricks[index] = colorCode
return {bricks: bricks.join('')}
}