first overlay render

This commit is contained in:
HF 2024-02-04 14:33:19 +01:00
parent ddd692bcbd
commit 7a15fa556b
8 changed files with 112 additions and 163 deletions

View File

@ -1,129 +0,0 @@
/*
* Settings of minimap / overlay
*/
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { t } from 'ttag';
import { coordsFromUrl } from '../core/utils';
import templateLoader from '../ui/templateLoader';
const AddTemplate = ({ close, triggerClose: refClose }) => {
const [render, setRender] = useState(false);
const [coords, setCoords] = useState(null);
const [dimensions, setDimensions] = useState(null);
const [title, setTitle] = useState('');
const [file, setFile] = useState(null);
const imgRef = useRef();
const [
canvasId,
canvases,
] = useSelector((state) => [
state.canvas.canvasId,
state.canvas.canvases,
], shallowEqual);
const [selectedCanvas, selectCanvas] = useState(canvasId);
useEffect(() => {
window.setTimeout(() => setRender(true), 10);
refClose.current = () => setRender(false);
}, [refClose]);
useEffect(() => {
if (!file || !imgRef.current) {
return;
}
const fr = new FileReader();
fr.onload = () => { imgRef.current.src = fr.result; };
fr.readAsDataURL(file);
}, [file]);
const canSubmit = (imgRef.current && file && coords && title && dimensions);
return (
<div
className="inarea"
style={{
opacity: render ? 1 : 0,
transition: 'opacity 200ms',
}}
onTransitionEnd={() => !render && close()}
>
<form
onSubmit={async (event) => {
event.preventDefault();
if (canSubmit) {
await templateLoader.addFile(
file, title, selectedCanvas, ...coords, imgRef.current,
);
setRender(false);
}
}}
>
<img
src="./logo.svg"
alt="preview"
key="logo"
style={{
maxWidth: 96,
maxHeight: 96,
}}
onLoad={(evt) => setDimensions([
evt.target.naturalWidth,
evt.target.naturalHeight,
])}
onError={() => setDimensions(null)}
ref={imgRef}
/>
<input
type="file"
onChange={(evt) => {
setDimensions(null);
setFile(evt.target.files?.[0]);
}}
/>
<input
value={title}
type="text"
onChange={(evt) => setTitle(evt.target.value)}
placeholder={t`Template Name`}
/>
<select
value={selectedCanvas}
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
<option key={canvas} value={canvas}>
{canvases[canvas].title}
</option>
))}
</select>
<input
type="text"
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
placeholder="X_Y or URL"
onChange={(evt) => {
let co = evt.target.value.trim();
co = coordsFromUrl(co) || co;
evt.target.value = co;
const newCoords = co.split('_').map((z) => parseInt(z, 10));
setCoords((!newCoords.some(Number.isNaN) && newCoords.length === 2)
? newCoords : null,
);
}}
/>
<button type="submit" disabled={!canSubmit}>{t`Save`}</button>
</form>
</div>
);
};
export default React.memo(AddTemplate);

View File

@ -127,11 +127,14 @@ const TemplateItemEdit = ({
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
<option key={canvas} value={canvas}>
{canvases[canvas].title}
</option>
))}
{Object.keys(canvases)
.filter((c) => !canvases[c].v && !canvases[c].ed)
.map((canvas) => (
<option key={canvas} value={canvas}>
{canvases[canvas].title}
</option>
),
)}
</select></span>
</p>
<p>

View File

@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
import fileDownload from 'js-file-download';
import { t } from 'ttag';
import AddTemplate from './AddTemplate';
import TemplateItem from './TemplateItem';
import TemplateItemEdit from './TemplateItemEdit';
import templateLoader from '../ui/templateLoader';
@ -35,7 +34,7 @@ const TemplateSettings = () => {
<>
<h3>{t`Templates`}</h3>
<p>
{t`Tired of always spaming one single color? Want to create art instead, but you have to count pixels from some other image? Templates can help you with that! Templates can show as overlay and you can draw over them. One pixel on the template, should be one pixel on the canvas.`}
{t`Tired of always spaming one single color? Want to create art instead, but you have to count pixels from some other image? Templates can help you with that! Templates can show as overlay and you can draw over them. One pixel on the template, should be one pixel on the canvas.`}
</p>
<div className="content">
{list.map(({
@ -103,17 +102,17 @@ const TemplateSettings = () => {
tabIndex={-1}
className="modallink"
onClick={async () => importRef.current?.click()}
>{t`Import templates`}</span>
<input
type="file"
key="impin"
accept="image/*"
ref={importRef}
style={{ display: 'none' }}
onChange={(evt) => {
templateLoader.importTemplates(evt.target.files?.[0]);
}}
/>
>{t`Import templates`}</span>
<input
type="file"
key="impin"
accept="image/*"
ref={importRef}
style={{ display: 'none' }}
onChange={(evt) => {
templateLoader.importTemplates(evt.target.files?.[0]);
}}
/>
</div>
</>
);

View File

@ -693,20 +693,20 @@ export function bufferToBase64(array) {
return new Promise((resolve) => {
const blob = new Blob([array]);
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target.result;
const [_, base64] = dataUrl.split(',');
resolve(base64);
};
reader.readAsDataURL(blob);
});
}
export async function base64ToBuffer(base64) {
const dataUrl = "data:application/octet-binary;base64," + base64;
const dataUrl = `data:application/octet-binary;base64,${base64}`;
const res = await fetch(dataUrl);
return res.arrayBuffer();
}

View File

@ -99,7 +99,7 @@ export default function templates(
],
};
}
case 'TEMPLATES_READY':
return {
...state,

View File

@ -22,6 +22,7 @@ import {
renderGrid,
renderPlaceholder,
renderPotatoPlaceholder,
renderOverlay,
} from './render2Delements';
import PixelPainterControls from '../controls/PixelPainterControls';
@ -272,6 +273,7 @@ class Renderer2D extends Renderer {
const [x, y] = getPixelFromChunkOffset(i, j, offset, canvasSize);
// TODO centerChunk is scaled!
const [canX, canY] = this.centerChunk
.map((z) => (z + 0.5) * TILE_SIZE - canvasSize / 2);
const { width: canvasWidth, height: canvasHeight } = this.canvas;
@ -340,9 +342,7 @@ class Renderer2D extends Renderer {
viewport,
viewscale: scale,
} = this;
const {
canvasSize,
} = state.canvas;
const { canvasSize } = state.canvas;
let { relScale } = this;
@ -422,6 +422,11 @@ class Renderer2D extends Renderer {
}
}
context.restore();
// TODO conditions
renderOverlay(
this.canvas, chunkPosition, canvasSize, scale,
this.tiledScale, this.scaleThreshold,
);
}

View File

@ -3,7 +3,9 @@
*
*/
import templateLoader from './templateLoader';
import { screenToWorld, worldToScreen } from '../core/utils';
import { TILE_SIZE } from '../core/constants';
const PLACEHOLDER_SIZE = 1.2;
const PLACEHOLDER_BORDER = 1;
@ -100,3 +102,46 @@ export function renderGrid(
viewportCtx.globalAlpha = 1;
}
/*
* Overlay draws onto offscreen canvas, so its doing weirder math
*/
export function renderOverlay(
$canvas,
centerChunk,
canvasSize,
scale,
tiledScale,
scaleThreshold,
) {
// world coordinates of center of center chunk
const [x, y] = centerChunk
.map((z) => z * TILE_SIZE / tiledScale
+ TILE_SIZE / 2 / tiledScale - canvasSize / 2);
const { width, height } = $canvas;
const horizontalRadius = width / 2 / scale;
const verticalRadius = height / 2 / scale;
const templates = templateLoader.getTemplatesInView(
x, y, horizontalRadius, verticalRadius,
);
if (!templates.length) return;
const context = $canvas.getContext('2d');
if (!context) return;
// if scale > scaleThreshold, then scaling happens in renderer
// instead of offscreen canvas
const offscreenScale = (scale > scaleThreshold) ? 1.0 : scale;
context.save();
context.scale(offscreenScale, offscreenScale);
for (const template of templates) {
const image = templateLoader.getTemplateSync(template.imageId);
if (!image) continue;
context.drawImage(image,
template.x - x + width / 2 / offscreenScale,
template.y - y + height / 2 / offscreenScale,
);
}
context.restore();
}

View File

@ -50,11 +50,35 @@ class TemplateLoader {
return null;
}
/*
getTemplatesInView() {
this.#store.templates
getTemplateSync(id) {
if (!this.ready) {
return null;
}
const template = this.#templates.get(id);
if (template) {
return template.image;
}
// TODO some store action when available
this.loadExistingTemplate(id);
return null;
}
getTemplatesInView(x, y, horizontalRadius, verticalRadius) {
const topX = x - horizontalRadius;
const topY = y - verticalRadius;
const bottomX = x + horizontalRadius;
const bottomY = y + verticalRadius;
const templates = [];
this.#store.getState().templates.list.forEach((template) => {
if (x < bottomX && y < bottomY
&& x + template.width > topX && y + template.height > topY
) {
templates.push(template);
}
});
return templates;
}
*/
/*
* sync database to store,
@ -152,7 +176,7 @@ class TemplateLoader {
deleteTemplate(title) {
const { list } = this.#store.getState().templates;
const tData = list.find((z) => z.title === title)
const tData = list.find((z) => z.title === title);
if (!tData) {
return;
}
@ -161,7 +185,7 @@ class TemplateLoader {
this.#fileStorage.deleteFile(imageId);
this.#templates.delete(imageId);
}
async exportEnabledTemplates() {
const { list } = this.#store.getState().templates;
const tDataList = list.filter((z) => z.enabled);
@ -181,7 +205,7 @@ class TemplateLoader {
}
return serilizableObj;
}
async importTemplates(file) {
const tDataList = JSON.parse(await file.text());
const bufferList = await Promise.all(
@ -197,7 +221,9 @@ class TemplateLoader {
const imageIdList = await this.#fileStorage.saveFile(fileList);
const idsToDelete = [];
for (let i = 0; i < tDataList.length; i += 1) {
const { x, y, width, height, canvasId, title } = tDataList[i];
const {
x, y, width, height, canvasId, title,
} = tDataList[i];
const imageId = imageIdList[i];
const existing = list.find((z) => z.title === title);
if (existing) {