Add files via upload
No files matched your search
|
After Width: | Height: | Size: 815 B |
|
After Width: | Height: | Size: 851 B |
|
After Width: | Height: | Size: 808 B |
|
After Width: | Height: | Size: 850 B |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 951 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 936 B |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 943 B |
|
After Width: | Height: | Size: 996 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 422 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,273 @@
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
// ndsbios.ttf uses 1024 units/em and a 64-unit pixel grid: 16px is
|
||||
// its native size. The alphabetic baseline is 11px; allow 14px per line.
|
||||
const FONT_SIZE = 16;
|
||||
const BASELINE = 11;
|
||||
const LINE_HEIGHT = 14;
|
||||
const FONT = '16px "Flipnote Text NDS BIOS"';
|
||||
let fontPromise;
|
||||
|
||||
function loadFont() {
|
||||
if (!fontPromise) {
|
||||
const face = new FontFace('Flipnote Text NDS BIOS', 'url("./assets/fonts/ndsbios.ttf")');
|
||||
const arabicFace = new FontFace('Flipnote Text Paxalah', 'url("./assets/fonts/paxalah.otf")');
|
||||
const arabic = global.FlipnoteArabicText ? Promise.resolve() : new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = './assets/flipnote-editor/arabic-text.js?v=1.1';
|
||||
script.onload = resolve; script.onerror = reject; document.head.append(script);
|
||||
});
|
||||
fontPromise = Promise.all([face, arabicFace].map(font => font.load().then(loaded => document.fonts.add(loaded))))
|
||||
.then(() => arabic).then(() => global.FlipnoteArabicText.load());
|
||||
}
|
||||
return fontPromise;
|
||||
}
|
||||
|
||||
function layoutText(value, anchor, width, measure, caretIndex = value.length) {
|
||||
const glyphs = [];
|
||||
let x = anchor.x, y = anchor.y, index = 0;
|
||||
let caret = {x, y};
|
||||
for (const character of value) {
|
||||
if (character === '\n') {
|
||||
x = anchor.x;
|
||||
y += LINE_HEIGHT;
|
||||
} else {
|
||||
const text = character === '\t' ? ' ' : character;
|
||||
const advance = Math.max(0, Math.round(measure(text)));
|
||||
if (x > anchor.x && x + advance > width) {
|
||||
x = anchor.x;
|
||||
y += LINE_HEIGHT;
|
||||
if (index === caretIndex) caret = {x, y};
|
||||
}
|
||||
glyphs.push({text, x, y});
|
||||
x += advance;
|
||||
}
|
||||
index += character.length;
|
||||
if (index <= caretIndex) caret = {x, y};
|
||||
}
|
||||
return {glyphs, caret};
|
||||
}
|
||||
|
||||
function rasterize(context, glyphs, width = 256, height = 192) {
|
||||
if (glyphs.some(glyph => glyph.bitmap)) return global.FlipnoteArabicText.rasterize(glyphs, width, height);
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.font = FONT;
|
||||
context.fontKerning = 'none';
|
||||
context.textBaseline = 'alphabetic';
|
||||
context.fillStyle = '#000';
|
||||
for (const glyph of glyphs) {
|
||||
if (glyph.y >= height) break;
|
||||
context.fillText(glyph.text, glyph.x, glyph.y + BASELINE);
|
||||
}
|
||||
const pixels = context.getImageData(0, 0, width, height).data;
|
||||
return Uint8Array.from({length: width * height}, (_, index) => pixels[index * 4 + 3] >= 128 ? 1 : 0);
|
||||
}
|
||||
|
||||
function subtitleMask(value) {
|
||||
if (global.FlipnoteArabicText.contains(value)) {
|
||||
const result = global.FlipnoteArabicText.subtitleMask(value);
|
||||
return {...result, ring: outline(result.mask)};
|
||||
}
|
||||
const surface = document.createElement('canvas');
|
||||
surface.width = 256; surface.height = 192;
|
||||
const context = surface.getContext('2d', {willReadFrequently: true});
|
||||
context.font = FONT; context.fontKerning = 'none';
|
||||
const measure = text => Array.from(text).reduce((sum, char) => sum + Math.round(context.measureText(char).width), 0);
|
||||
const lines = [];
|
||||
for (const paragraph of value.split('\n')) {
|
||||
let line = '';
|
||||
for (const word of paragraph.split(' ')) {
|
||||
if (line && measure((line + ' ' + word).trim()) > 244) { lines.push(line); line = ''; }
|
||||
for (const char of (line ? ' ' : '') + word) {
|
||||
if (line && measure(line + char) > 244) { lines.push(line.trimEnd()); line = ''; }
|
||||
line += char;
|
||||
}
|
||||
}
|
||||
lines.push(line.trim());
|
||||
}
|
||||
if (lines.length > 2) throw new Error('Keep each subtitle to two lines. Split longer text into another subtitle.');
|
||||
const top = 192 - 4 - 12 - (lines.length - 1) * LINE_HEIGHT;
|
||||
const glyphs = [];
|
||||
lines.forEach((line, index) => {
|
||||
let x = Math.floor((256 - measure(line)) / 2);
|
||||
for (const char of line) {
|
||||
glyphs.push({text: char, x, y: top + index * LINE_HEIGHT});
|
||||
x += measure(char);
|
||||
}
|
||||
});
|
||||
const mask = rasterize(context, glyphs);
|
||||
return {mask, ring: outline(mask), lines};
|
||||
}
|
||||
|
||||
function outline(mask) {
|
||||
const ring = new Uint8Array(mask.length);
|
||||
for (let y = 0; y < 192; y++) for (let x = 0; x < 256; x++) {
|
||||
if (!mask[y * 256 + x]) continue;
|
||||
for (let yy = Math.max(0, y - 1); yy <= Math.min(191, y + 1); yy++)
|
||||
for (let xx = Math.max(0, x - 1); xx <= Math.min(255, x + 1); xx++)
|
||||
if (!mask[yy * 256 + xx]) ring[yy * 256 + xx] = 1;
|
||||
}
|
||||
return ring;
|
||||
}
|
||||
|
||||
class FlipnoteTextTool {
|
||||
constructor(editor, timeline, hooks) {
|
||||
this.editor = editor;
|
||||
this.timeline = timeline;
|
||||
this.hooks = hooks;
|
||||
this.enabled = false;
|
||||
this.session = null;
|
||||
this.request = 0;
|
||||
this.fontLoaded = false;
|
||||
this.fontError = false;
|
||||
this.ready = loadFont().then(() => { this.fontLoaded = true; }).catch(() => { this.fontError = true; });
|
||||
this.raster = document.createElement('canvas');
|
||||
this.raster.width = editor.width;
|
||||
this.raster.height = editor.height;
|
||||
this.context = this.raster.getContext('2d', {willReadFrequently: true});
|
||||
this.input = document.createElement('textarea');
|
||||
this.input.className = 'flipnote-editor-text-input';
|
||||
this.input.setAttribute('aria-label', 'Flipnote text. Press Escape to finish.');
|
||||
this.input.setAttribute('dir', 'auto');
|
||||
this.input.setAttribute('autocapitalize', 'off');
|
||||
this.input.setAttribute('autocomplete', 'off');
|
||||
this.input.setAttribute('autocorrect', 'off');
|
||||
this.input.spellcheck = false;
|
||||
this.input.maxLength = 4096;
|
||||
this.input.tabIndex = -1;
|
||||
document.body.append(this.input);
|
||||
this.input.addEventListener('input', () => this.update());
|
||||
this.input.addEventListener('keydown', (event) => {
|
||||
event.stopPropagation();
|
||||
if (event.key !== 'Escape' || event.isComposing) return;
|
||||
event.preventDefault();
|
||||
this.finish();
|
||||
editor.canvasElement.focus({preventScroll: true});
|
||||
});
|
||||
this.input.addEventListener('keyup', () => this.resetCaret());
|
||||
this.input.addEventListener('blur', () => this.finish());
|
||||
document.addEventListener('selectionchange', () => {
|
||||
if (document.activeElement === this.input) this.resetCaret();
|
||||
});
|
||||
document.addEventListener('pointerdown', (event) => {
|
||||
if (event.target !== editor.canvasElement && event.target !== this.input) this.finish();
|
||||
}, true);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.enabled) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.editor.finishStroke();
|
||||
this.editor.clearSelection();
|
||||
this.enabled = true;
|
||||
this.editor.updateCursorMode();
|
||||
this.editor.renderCurrentFrame();
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.finish();
|
||||
this.enabled = false;
|
||||
this.request++;
|
||||
this.editor.updateCursorMode();
|
||||
this.editor.renderCurrentFrame();
|
||||
}
|
||||
|
||||
async begin(point) {
|
||||
const request = ++this.request;
|
||||
const frame = this.timeline.getFrame(this.timeline.currentFrame);
|
||||
const layer = this.editor.activeLayer;
|
||||
await this.ready;
|
||||
if (!this.enabled || request !== this.request || !this.editor.allowEditing || this.timeline.playing
|
||||
|| frame !== this.timeline.getFrame(this.timeline.currentFrame) || layer !== this.editor.activeLayer) return;
|
||||
if (this.fontError) {
|
||||
global.alert('The text fonts could not load. Reload the editor before using the Text tool.');
|
||||
return;
|
||||
}
|
||||
this.finish();
|
||||
this.session = {
|
||||
frame, layer, base: frame.layers[layer].slice(), value: '',
|
||||
anchor: {x: Math.max(0, Math.min(this.editor.width - 1, Math.floor(point.x))),
|
||||
y: Math.max(0, Math.min(this.editor.height - 1, Math.floor(point.y)))},
|
||||
};
|
||||
this.input.value = '';
|
||||
this.input.focus({preventScroll: true});
|
||||
this.resetCaret();
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (global.FlipnoteArabicText.contains(this.input.value)) {
|
||||
return global.FlipnoteArabicText.layoutText(this.input.value, this.session.anchor, this.editor.width, this.input.selectionEnd);
|
||||
}
|
||||
this.context.font = FONT;
|
||||
this.context.fontKerning = 'none';
|
||||
return layoutText(this.input.value, this.session.anchor, this.editor.width,
|
||||
(text) => this.context.measureText(text).width, this.input.selectionEnd);
|
||||
}
|
||||
|
||||
update() {
|
||||
const session = this.session;
|
||||
if (!session) return;
|
||||
const layer = session.frame.layers[session.layer];
|
||||
const previous = layer.slice();
|
||||
const ctx = this.context;
|
||||
const layout = this.layout();
|
||||
const mask = rasterize(ctx, layout.glyphs, this.editor.width, this.editor.height);
|
||||
for (let i = 0; i < layer.length; i++) layer[i] = session.base[i] || mask[i];
|
||||
if (!this.hooks.canStoreFrame(session.frame)) {
|
||||
layer.set(previous);
|
||||
this.input.value = session.value;
|
||||
this.hooks.notifyFull();
|
||||
} else {
|
||||
session.value = this.input.value;
|
||||
this.hooks.markDirty();
|
||||
}
|
||||
this.timeline.createThumbnails();
|
||||
this.resetCaret();
|
||||
}
|
||||
|
||||
resetCaret() {
|
||||
this.caretStartedAt = Date.now();
|
||||
this.editor.renderCurrentFrame();
|
||||
}
|
||||
|
||||
renderCaret() {
|
||||
if (!this.session || this.timeline.playing || document.activeElement !== this.input) return;
|
||||
const caret = this.layout().caret;
|
||||
const rect = this.editor.canvasElement.getBoundingClientRect();
|
||||
this.input.style.left = `${rect.left + Math.min(caret.x, this.editor.width - 1) * rect.width / this.editor.width}px`;
|
||||
this.input.style.top = `${rect.top + Math.min(caret.y, this.editor.height - 1) * rect.height / this.editor.height}px`;
|
||||
if ((Date.now() - this.caretStartedAt) % 1000 >= 500 || caret.y >= this.editor.height) return;
|
||||
const ctx = this.editor.ctx;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = 'difference';
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(Math.max(0, Math.min(caret.x, this.editor.width - 1)), caret.y, 1, caret.height || BASELINE + 1);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.request++;
|
||||
const session = this.session;
|
||||
if (!session) return;
|
||||
this.session = null;
|
||||
const layer = session.frame.layers[session.layer];
|
||||
const changed = layer.some((pixel, i) => pixel !== session.base[i]);
|
||||
if (changed) {
|
||||
const index = this.timeline.frames.indexOf(session.frame);
|
||||
if (index >= 0) this.timeline.updateFrame(index);
|
||||
}
|
||||
if (document.activeElement === this.input) this.input.blur();
|
||||
this.editor.renderCurrentFrame();
|
||||
}
|
||||
}
|
||||
|
||||
FlipnoteTextTool.layoutText = layoutText;
|
||||
FlipnoteTextTool.loadFont = loadFont;
|
||||
FlipnoteTextTool.subtitleMask = subtitleMask;
|
||||
FlipnoteTextTool.metrics = {fontSize: FONT_SIZE, baseline: BASELINE, lineHeight: LINE_HEIGHT};
|
||||
global.FlipnoteTextTool = FlipnoteTextTool;
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 51 KiB |
