feat: create nice image defaults

backup
flop 3 weeks ago
parent d50e17b1cc
commit 8dc2a5c3d8
  1. 124
      ts/src/browser.ts
  2. 37
      ts/style.css

@ -146,7 +146,14 @@ function createNewElement(type: MonoDisplay.ElementType): MonoDisplay.MonoFormat
const flags: Record<string, any> = {};
(EL_FLAGS[MonoDisplay.ElementTypeToString[type]] as ElFlagMeta[] || [])
.forEach(f => (flags[f.key] = f.default ?? false));
return { type, ...fields, flags } as MonoDisplay.MonoFormatElement;
const el: any = { type, ...fields, flags };
if (type === MonoDisplay.ElementType.Image2D) {
el.image = { pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height };
}
if (type === MonoDisplay.ElementType.Animation) {
el.frames = [{ pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height }];
}
return el as MonoDisplay.MonoFormatElement;
}
function getSectionByIndex(i: number | null): MonoDisplay.MonoFormatSection | undefined {
@ -416,6 +423,8 @@ function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoDisplay.
...DEFAULT_FONTS.map((name, i) => m("option", { value: i, selected: val === i }, name)),
...getCustomFonts().map(cf => m("option", { value: cf.index, selected: val === cf.index }, cf.fontname)),
]);
case "list":
return m("span"); // rendered by dedicated editor
default:
return m("input[type=number]", {
value: val,
@ -424,7 +433,7 @@ function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoDisplay.
}
}
// ─── Image2D pixel editor ─────────────────────────────────────────────────────
// --- Shared pixel canvas ------------------------------------------------------
const PIXEL_SCALE = 4;
@ -432,7 +441,7 @@ function getPixelIndex(img: MonoDisplay.MonoFormatPixelImage, x: number, y: numb
return y * img.width + x;
}
function drawImage2DCanvas(canvas: HTMLCanvasElement, img: MonoDisplay.MonoFormatPixelImage) {
function drawPixelCanvas(canvas: HTMLCanvasElement, img: MonoDisplay.MonoFormatPixelImage) {
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
@ -446,23 +455,13 @@ function drawImage2DCanvas(canvas: HTMLCanvasElement, img: MonoDisplay.MonoForma
}
}
interface Image2DEditorState {
drawing: boolean;
drawValue: number; // 0 or 1
}
interface PixelCanvasState { drawing: boolean; drawValue: number; }
const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatImage2D }, Image2DEditorState> = {
const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: () => void }, PixelCanvasState> = {
drawing: false,
drawValue: 1,
view({ attrs: { si, ei, el }, state }) {
if (!el.image) {
const w = (el as any).width || W;
const h = (el as any).height || H;
el.image = { pixels: new Uint8Array(w * h), width: w, height: h };
}
const img = el.image;
view({ attrs: { img, onpaint }, state }) {
function pixelFromEvent(e: MouseEvent): { x: number; y: number } | null {
const canvas = e.currentTarget as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect();
@ -477,20 +476,16 @@ const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoF
function paint(e: MouseEvent) {
const p = pixelFromEvent(e);
if (!p) return;
const idx = getPixelIndex(img, p.x, p.y);
img.pixels[idx] = state.drawValue;
markDirty(); triggerPreview();
drawImage2DCanvas(e.currentTarget as HTMLCanvasElement, img);
img.pixels[getPixelIndex(img, p.x, p.y)] = state.drawValue;
onpaint();
drawPixelCanvas(e.currentTarget as HTMLCanvasElement, img);
}
return m("canvas.pixel-editor", {
width: img.width * PIXEL_SCALE,
height: img.height * PIXEL_SCALE,
oncreate: ({ dom }: m.VnodeDOM) => {
(dom as any)._el = el;
drawImage2DCanvas(dom as HTMLCanvasElement, img);
},
onupdate: ({ dom }: m.VnodeDOM) => drawImage2DCanvas(dom as HTMLCanvasElement, img),
oncreate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img),
onupdate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img),
onmousedown: (e: MouseEvent) => {
state.drawing = true;
const p = pixelFromEvent(e);
@ -504,6 +499,80 @@ const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoF
},
};
// --- Image2D editor -----------------------------------------------------------
const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatImage2D }> = {
view({ attrs: { si, ei, el } }) {
if (!el.image) {
const w = (el as any).width || W;
const h = (el as any).height || H;
el.image = { pixels: new Uint8Array(w * h), width: w, height: h };
}
return m(PixelCanvas, {
img: el.image,
onpaint: () => { markDirty(); triggerPreview(); },
});
},
};
// --- Animation editor ---------------------------------------------------------
interface AnimationEditorState { activeFrame: number; }
const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatAnimation }, AnimationEditorState> = {
activeFrame: 0,
view({ attrs: { si, ei, el }, state }) {
const w = el.width || W;
const h = el.height || H;
if (!el.frames) el.frames = [];
if (!el.frames.length) {
el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
}
state.activeFrame = Math.min(state.activeFrame, el.frames.length - 1);
const frame = el.frames[state.activeFrame];
function addFrame() {
el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
state.activeFrame = el.frames.length - 1;
markDirty(); triggerPreview(); m.redraw();
}
function removeFrame(fi: number) {
el.frames.splice(fi, 1);
if (!el.frames.length)
el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
state.activeFrame = Math.min(state.activeFrame, el.frames.length - 1);
markDirty(); triggerPreview();
}
return m(".anim-editor",
m(".anim-frame-tabs",
el.frames.map((_, fi) =>
m(".anim-frame-tab", {
class: fi === state.activeFrame ? "active" : "",
onclick: () => { state.activeFrame = fi; },
},
m("span", fi + 1),
el.frames.length > 1
? m("button.x-btn-sm", {
onclick: (e: Event) => { e.stopPropagation(); removeFrame(fi); },
}, "×")
: null,
)
),
m("button.add-el", { onclick: addFrame }, "+ frame"),
),
m("div", { key: state.activeFrame },
m(PixelCanvas, {
img: frame,
onpaint: () => { markDirty(); triggerPreview(); },
}),
),
);
},
};
const ElementItem: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatElement }> = {
view({ attrs: { si, ei, el } }) {
const isActive = activeSecIndex === si && activeElIndex === ei;
@ -558,6 +627,11 @@ const ElementItem: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFor
m("label", "Pixels"),
m(Image2DEditor, { si, ei, el: el as MonoDisplay.MonoFormatImage2D }),
)
: el.type === MonoDisplay.ElementType.Animation
? m(".field.full",
m("label", "Frames"),
m(AnimationEditor, { si, ei, el: el as MonoDisplay.MonoFormatAnimation }),
)
: null,
),
);

@ -263,6 +263,43 @@ canvas.pixel-editor {
max-width: 100%;
border: 1px solid var(--bd-inner);
}
.anim-editor {
display: flex;
flex-direction: column;
gap: 4px;
}
.anim-frame-tabs {
display: flex;
flex-wrap: wrap;
gap: 3px;
align-items: center;
}
.anim-frame-tab {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border: 1px solid var(--bd-inner);
border-radius: 3px;
cursor: pointer;
font-size: 11px;
user-select: none;
}
.anim-frame-tab.active {
border-color: var(--accent, #EC0);
color: var(--accent, #EC0);
}
.x-btn-sm {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0 2px;
font-size: 13px;
line-height: 1;
opacity: 0.6;
}
.x-btn-sm:hover { opacity: 1; }
#sec-meta {
background: var(--bg-sunken);

Loading…
Cancel
Save