From bc2821adb11a30244fc4663f4fafb756877d9508 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:09:40 +0900 Subject: bluebey-studio: public/ の外へ移動し非公開化 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/gion.js | 226 -------------------------------------- 1 file changed, 226 deletions(-) delete mode 100644 public/bluebey-studio/src/gion.js (limited to 'public/bluebey-studio/src/gion.js') diff --git a/public/bluebey-studio/src/gion.js b/public/bluebey-studio/src/gion.js deleted file mode 100644 index 832313b..0000000 --- a/public/bluebey-studio/src/gion.js +++ /dev/null @@ -1,226 +0,0 @@ -/** - * 擬音 (マンガのオノマトペ) のスタンプ. - * - * The source images are dense sheets: several sounds, each in a white-outline and - * a solid version, packed so tightly that an automatic slice (outline tracing / - * connected components) tears a single character into fragments. So nothing here - * guesses at a sheet's layout. Instead the user draws a rectangle over the sheet - * in the picker and the app crops exactly that rectangle, which means the feature - * works for any sheet that arrives later. - * - * That is why the crops are stored as *source pixels* (`sx, sy, sw, sh`) taken - * from the bitmap's own `naturalWidth` / `naturalHeight`, never as fractions of - * some assumed grid: the sheet's pixel size is read at runtime. - * - * The module is pure and DOM-free in the same sense as `trace.js` and - * `caption.js`: no globals are touched except a read of the injected - * `__BLUEBEY_GIONS__` lookup, and `drawGion` takes the loaded bitmaps as an - * argument, so the caller owns loading and the tests can drive the geometry in - * Node with a stub context. - */ - -/** - * The sheets the studio offers. It ships with none: the original otarunet sheet - * may not be redistributed with the app, and the images the author draws later are - * added here. So this list starts empty and the 擬音 section stays out of the panel - * until an entry appears. - * - * To add one: put the image in `assets/manga-gion/`, add `{ name, label }` here - * (`name` is the file name *with* its extension, e.g. `dokaan.png`) and add the - * same `name` to `GION_NAMES` in `tools/build-standalone.mjs` for the one-file - * build. Use an image with a transparent background - the crop is drawn as-is, so - * a white background would sit on the picture as a white rectangle. - */ -export const GION_SHEETS = []; - -/** - * The default display width of a stamp, in pixels at 1x. A crop that is tall and - * thin will be narrower than this after the aspect is applied; the user resizes - * it from the panel either way. - */ -export const DEFAULT_STAMP_WIDTH = 240; - -/** - * Where a sheet's image lives. The single-file build inlines every sheet as a - * data URL in `window.__BLUEBEY_GIONS__`; the normal build reads the file. - * - * @param {string} name the sheet file name, with its extension, e.g. `dokaan.png` - * @returns {string} - */ -export function gionSheetUrl(name) { - const inlined = globalThis.__BLUEBEY_GIONS__ ?? {}; - return inlined[name] ?? `assets/manga-gion/${name}`; -} - -/** Clamp `value` into `lo..hi`; a non-finite value becomes `lo`. */ -export function clamp(value, lo, hi) { - if (!Number.isFinite(value)) return lo; - return Math.min(hi, Math.max(lo, value)); -} - -/** True when `image` has pixels to draw (a not-yet-loaded Image has none). */ -export function imageReady(image) { - if (!image) return false; - const width = Number(image.naturalWidth ?? image.width ?? 0); - const height = Number(image.naturalHeight ?? image.height ?? 0); - // `complete` is the browser's own answer; a stub in a test omits it, so only an - // explicit `false` blocks the draw. - return width > 0 && height > 0 && image.complete !== false; -} - -/** The bitmap's width / height. 1 when it is not loaded yet. */ -export function imageAspect(image) { - const width = toPositive(image?.naturalWidth ?? image?.width, 1); - const height = toPositive(image?.naturalHeight ?? image?.height, 1); - return width / height; -} - -/** - * The rectangle a stamp occupies on screen, in pixels, with rotation left out - * (the caller rotates about the returned centre). - * - * `item.x` / `item.y` are the centre as a fraction of the viewport, `item.w` is - * the width in pixels at 1x, and the height follows the bitmap's aspect ratio, - * so a wide crop and a tall crop are both placed by their width alone. - * - * @param {object} item a stamp: `{ x, y, w, ... }` - * @param {object} [viewport] `{ width, height, scale }` of the target surface - * @param {number} [aspect] bitmap width / height - * @returns {{x: number, y: number, w: number, h: number, cx: number, cy: number}} - */ -export function stampRect(item, { width = 0, height = 0, scale = 1 } = {}, aspect = 1) { - const w = toPositive(item?.w, DEFAULT_STAMP_WIDTH) * positiveScale(scale); - const h = w / toPositive(aspect, 1); - const cx = toFinite(item?.x, 0.5) * toNonNegative(width, 0); - const cy = toFinite(item?.y, 0.5) * toNonNegative(height, 0); - return { x: cx - w / 2, y: cy - h / 2, w, h, cx, cy }; -} - -/** Turn two corner points into a rectangle with a positive width and height. */ -export function normalizeRect(a, b) { - const ax = toFinite(a?.x, 0); - const ay = toFinite(a?.y, 0); - const bx = toFinite(b?.x, 0); - const by = toFinite(b?.y, 0); - return { x: Math.min(ax, bx), y: Math.min(ay, by), w: Math.abs(bx - ax), h: Math.abs(by - ay) }; -} - -/** - * Map a marquee (drawn in screen pixels over the fitted sheet) to a pixel crop of - * the source bitmap. - * - * `sheetRect` is where the sheet is currently displayed, so the marquee becomes a - * fraction of the sheet and then a fraction of the bitmap - the sheet's own pixel - * size comes from `natural`, never from a hard-coded number. The crop is clamped - * to the bitmap and is at least 1x1, so it can always be drawn. - * - * @param {{x: number, y: number, w: number, h: number}} marquee screen pixels - * @param {{x: number, y: number, w: number, h: number}} sheetRect screen pixels - * @param {object} natural the sheet image (or any `{ naturalWidth, naturalHeight }`) - * @returns {{sx: number, sy: number, sw: number, sh: number}} - */ -export function cropFromMarquee(marquee, sheetRect, natural) { - const nw = Math.max(1, Math.round(toPositive(natural?.naturalWidth, 1))); - const nh = Math.max(1, Math.round(toPositive(natural?.naturalHeight, 1))); - const rect = { - x: toFinite(marquee?.x, 0), - y: toFinite(marquee?.y, 0), - w: toNonNegative(marquee?.w, 0), - h: toNonNegative(marquee?.h, 0), - }; - const displayW = toNonNegative(sheetRect?.w, 0); - const displayH = toNonNegative(sheetRect?.h, 0); - - const left = displayW > 0 ? clamp((rect.x - sheetRect.x) / displayW, 0, 1) : 0; - const right = displayW > 0 ? clamp((rect.x + rect.w - sheetRect.x) / displayW, 0, 1) : 0; - const top = displayH > 0 ? clamp((rect.y - sheetRect.y) / displayH, 0, 1) : 0; - const bottom = displayH > 0 ? clamp((rect.y + rect.h - sheetRect.y) / displayH, 0, 1) : 0; - - const sx = clamp(Math.round(left * nw), 0, nw - 1); - const sy = clamp(Math.round(top * nh), 0, nh - 1); - return { - sx, - sy, - sw: clamp(Math.round((right - left) * nw), 1, nw - sx), - sh: clamp(Math.round((bottom - top) * nh), 1, nh - sy), - }; -} - -/** - * Fit a bitmap inside a box, keeping its aspect ratio and centring it. The picker - * uses this to show a whole sheet - however large - at a size the user can drag - * over. - * - * @param {object} natural the sheet image - * @param {{width: number, height: number}} box the available area, in pixels - * @param {object} [options] - * @param {number} [options.padding=0] a margin to keep inside the box - * @returns {{x: number, y: number, w: number, h: number}} - */ -export function fitSheet(natural, box, { padding = 0 } = {}) { - const nw = toPositive(natural?.naturalWidth ?? natural?.width, 1); - const nh = toPositive(natural?.naturalHeight ?? natural?.height, 1); - const pad = toNonNegative(padding, 0); - const availW = Math.max(0, toNonNegative(box?.width, 0) - pad * 2); - const availH = Math.max(0, toNonNegative(box?.height, 0) - pad * 2); - const scale = Math.min(availW / nw, availH / nh); - const w = nw * scale; - const h = nh * scale; - return { x: pad + (availW - w) / 2, y: pad + (availH - h) / 2, w, h }; -} - -/** - * Paint every stamp onto a 2D context. - * - * The same function runs over the live viewport overlay (scale 1) and into the - * exported PNG (scale = output pixels / CSS pixels), which is what keeps the - * preview and the file in step. A stamp whose sheet has not loaded yet is simply - * skipped, so the rest of the picture is never held up by one image. - * - * @param {CanvasRenderingContext2D} ctx - * @param {Array} items the stamps - * @param {Map} images loaded sheets, by name - * @param {object} [options] - * @param {number} [options.width=0] - * @param {number} [options.height=0] - * @param {number} [options.scale=1] - */ -export function drawGion(ctx, items, images, { width = 0, height = 0, scale = 1 } = {}) { - if (!ctx || !Array.isArray(items)) return; - const viewport = { width, height, scale: positiveScale(scale) }; - for (const item of items) { - const image = images?.get?.(item?.sheet); - if (!imageReady(image)) continue; - const rect = stampRect(item, viewport, imageAspect(image)); - if (!(rect.w > 0) || !(rect.h > 0)) continue; - const rot = toFinite(item?.rot, 0); - ctx.save(); - ctx.translate(rect.cx, rect.cy); - if (rot !== 0) ctx.rotate((rot * Math.PI) / 180); - // The flip is applied before the draw so the same source rect feeds both. - if (item?.flip) ctx.scale(-1, 1); - ctx.drawImage(image, item.sx, item.sy, item.sw, item.sh, -rect.w / 2, -rect.h / 2, rect.w, rect.h); - ctx.restore(); - } -} - -/* ----------------------------------------------------------------- numbers */ - -function toFinite(value, fallback) { - const n = Number(value); - return Number.isFinite(n) ? n : fallback; -} - -function toPositive(value, fallback) { - const n = Number(value); - return Number.isFinite(n) && n > 0 ? n : fallback; -} - -function toNonNegative(value, fallback) { - const n = Number(value); - return Number.isFinite(n) && n >= 0 ? n : fallback; -} - -function positiveScale(value) { - return toPositive(value, 1); -} -- cgit v1.3.1