aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/gion.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
commitbc2821adb11a30244fc4663f4fafb756877d9508 (patch)
treefea8d30d2eb1e67f895097dbf7ec4d152361243e /public/bluebey-studio/src/gion.js
parent0d50c5ede0812ba7b67b775c9cd0bf52d7b65e02 (diff)
bluebey-studio: public/ の外へ移動し非公開化
Diffstat (limited to 'public/bluebey-studio/src/gion.js')
-rw-r--r--public/bluebey-studio/src/gion.js226
1 files changed, 0 insertions, 226 deletions
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<object>} items the stamps
- * @param {Map<string, CanvasImageSource>} 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);
-}