Add files via upload
No files matched your search
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,213 @@
|
|||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
let data, bidi, loading;
|
||||||
|
const contains = value => /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/u.test(value);
|
||||||
|
function configure(value, factory) { data = value; bidi = factory(); }
|
||||||
|
function load() {
|
||||||
|
if (data && bidi) return Promise.resolve();
|
||||||
|
if (!loading) loading = Promise.all([
|
||||||
|
fetch('./assets/flipnote-editor/arabic-text.json?v=1.1').then(response => {
|
||||||
|
if (!response.ok) throw new Error('The Arabic text font could not load.');
|
||||||
|
return response.json();
|
||||||
|
}),
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (global.bidi_js) { resolve(global.bidi_js); return; }
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = './assets/flipnote-editor/bidi.min.js?v=1.0.3';
|
||||||
|
script.onload = () => resolve(global.bidi_js);
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.append(script);
|
||||||
|
}),
|
||||||
|
]).then(([value, factory]) => configure(value, factory));
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Joining forms follow arabic-reshaper's MIT-licensed algorithm.
|
||||||
|
// Keep the same forms, ligatures and mark handling as the PPM renderer.
|
||||||
|
function reshape(text) {
|
||||||
|
text = text.normalize('NFC');
|
||||||
|
const output = [], marks = new Map(), letters = data.letters;
|
||||||
|
for (const char of text) {
|
||||||
|
if (data.marks.includes(char)) {
|
||||||
|
const position = output.length - 1;
|
||||||
|
if (!marks.has(position)) marks.set(position, []);
|
||||||
|
marks.get(position).push(char);
|
||||||
|
} else if (!letters[char]) output.push([char, -1]);
|
||||||
|
else if (!output.length) output.push([char, 0]);
|
||||||
|
else {
|
||||||
|
const previous = output[output.length - 1];
|
||||||
|
const a = letters[char], b = letters[previous[0]];
|
||||||
|
if (previous[1] === -1 || !(a[3] || a[2]) || !(b[1] || b[2]) || (previous[1] === 3 && !b[2])) output.push([char, 0]);
|
||||||
|
else { previous[1] = previous[1] === 0 ? 1 : 2; output.push([char, 3]); }
|
||||||
|
}
|
||||||
|
if (output.length > 1 && output[output.length - 2][0] === '\u200d') output.splice(output.length - 2, 1);
|
||||||
|
}
|
||||||
|
if (output.length && output[output.length - 1][0] === '\u200d') output.pop();
|
||||||
|
const clean = Array.from(text).filter(char => !data.marks.includes(char)).join('');
|
||||||
|
for (const [word, forms] of data.ligatures) {
|
||||||
|
let start = clean.indexOf(word);
|
||||||
|
while (start !== -1) {
|
||||||
|
const end = start + word.length, first = output[start]?.[1], last = output[end - 1]?.[1];
|
||||||
|
const form = first === 0 || first === 1 ? (last === 0 || last === 3 ? 0 : 1) : (last === 0 || last === 3 ? 3 : 2);
|
||||||
|
if (forms[form] && output[start]) {
|
||||||
|
output[start] = [forms[form], -1];
|
||||||
|
for (let i = start + 1; i < end; i++) output[i] = ['', -1];
|
||||||
|
}
|
||||||
|
start = clean.indexOf(word, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (marks.get(-1) || []).join('') + output.map(([char, form], index) => (char && form !== -1 && form !== 0 ? letters[char][form] : char) + (marks.get(index) || []).join('')).join('');
|
||||||
|
}
|
||||||
|
function visualClusters(text) {
|
||||||
|
const clusters = [];
|
||||||
|
for (const char of reshape(text)) {
|
||||||
|
if (data.marks.includes(char) && clusters.length) clusters[clusters.length - 1].marks += char;
|
||||||
|
else clusters.push({char, marks: ''});
|
||||||
|
}
|
||||||
|
const base = clusters.map(cluster => cluster.char).join('');
|
||||||
|
const levels = bidi.getEmbeddingLevels(base);
|
||||||
|
const order = bidi.getReorderedIndices(base, levels);
|
||||||
|
const display = bidi.getReorderedString(base, levels);
|
||||||
|
return order.map((index, position) => ({char: display[position], marks: clusters[index].marks}));
|
||||||
|
}
|
||||||
|
function visual(text) {
|
||||||
|
return text.split('\n').map(line => visualClusters(line).map(cluster => cluster.char + cluster.marks).join('')).join('\n');
|
||||||
|
}
|
||||||
|
function markGlyphs(marks) {
|
||||||
|
const glyphs = [];
|
||||||
|
for (let index = 0; index < marks.length;) {
|
||||||
|
const match = data.mark_ligatures.find(([text]) => marks.startsWith(text, index));
|
||||||
|
if (match) { glyphs.push(data.glyphs[match[1]]); index += match[0].length; }
|
||||||
|
else glyphs.push(data.glyphs[marks[index++]]);
|
||||||
|
}
|
||||||
|
return glyphs;
|
||||||
|
}
|
||||||
|
function placeMark(bitmap, base, previous) {
|
||||||
|
let position;
|
||||||
|
for (const lookup of data.positioning) {
|
||||||
|
const reference = lookup.marks[bitmap.name];
|
||||||
|
if (!reference) continue;
|
||||||
|
const targets = lookup.kind === 'mark' ? previous.slice().reverse() : [base];
|
||||||
|
for (const target of targets) {
|
||||||
|
let anchors = lookup.bases[target.bitmap.name];
|
||||||
|
if (!anchors) continue;
|
||||||
|
if (lookup.kind === 'ligature') anchors = anchors[0];
|
||||||
|
const anchor = anchors[reference[0]];
|
||||||
|
if (!anchor) continue;
|
||||||
|
position = {bitmap,
|
||||||
|
x: target.x + Math.round((anchor[0] - reference[1]) * data.pixel_scale),
|
||||||
|
y: target.y - Math.round((anchor[1] - reference[2]) * data.pixel_scale)};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position || {bitmap, x: base.x + Math.floor(base.bitmap.advance / 2), y: base.y};
|
||||||
|
}
|
||||||
|
function line(text) {
|
||||||
|
const glyphs = [];
|
||||||
|
let pen = 0;
|
||||||
|
for (const cluster of visualClusters(text)) {
|
||||||
|
if (cluster.char === '\u200c' || cluster.char === '\u200d') continue;
|
||||||
|
const bitmap = data.glyphs[cluster.char];
|
||||||
|
if (!bitmap) throw new Error(`The text fonts do not include “${cluster.char}”. Replace it to save this subtitle.`);
|
||||||
|
const base = {bitmap, x: pen, y: 0};
|
||||||
|
glyphs.push(base);
|
||||||
|
const marks = [];
|
||||||
|
for (const mark of markGlyphs(cluster.marks)) {
|
||||||
|
const placed = placeMark(mark, base, marks);
|
||||||
|
glyphs.push(placed); marks.push(placed);
|
||||||
|
}
|
||||||
|
pen += bitmap.advance;
|
||||||
|
}
|
||||||
|
let left = 0, right = pen, above = 11, below = 1;
|
||||||
|
for (const glyph of glyphs) {
|
||||||
|
for (const [_dy, dx, count] of glyph.bitmap.spans) {
|
||||||
|
left = Math.min(left, glyph.x + dx); right = Math.max(right, glyph.x + dx + count);
|
||||||
|
}
|
||||||
|
above = Math.max(above, -glyph.bitmap.top - glyph.y);
|
||||||
|
below = Math.max(below, glyph.bitmap.bottom + glyph.y);
|
||||||
|
}
|
||||||
|
if (left) glyphs.forEach(glyph => { glyph.x -= left; });
|
||||||
|
return {glyphs, advance: pen, width: right - left, above, height: Math.max(14, above + below + 2)};
|
||||||
|
}
|
||||||
|
function paint(mask, width, height, glyph, x, baseline) {
|
||||||
|
for (const [dy, dx, count] of glyph.spans) {
|
||||||
|
const y = baseline + dy;
|
||||||
|
if (y < 0 || y >= height) continue;
|
||||||
|
const start = Math.max(0, x + dx), end = Math.min(width, x + dx + count);
|
||||||
|
if (end > start) mask.fill(1, y * width + start, y * width + end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function rasterize(glyphs, width, height) {
|
||||||
|
const mask = new Uint8Array(width * height);
|
||||||
|
for (const glyph of glyphs) paint(mask, width, height, glyph.bitmap, glyph.x, glyph.baseline);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
function wrap(text, width) {
|
||||||
|
const lines = [];
|
||||||
|
for (const paragraph of text.split('\n')) {
|
||||||
|
let current = '';
|
||||||
|
for (const word of paragraph.split(' ')) {
|
||||||
|
if (current && line((current + ' ' + word).trim()).width > width) { lines.push(current); current = ''; }
|
||||||
|
for (const char of graphemes((current ? ' ' : '') + word)) {
|
||||||
|
if (current && line(current + char).width > width) { lines.push(current.trimEnd()); current = ''; }
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(current.trim());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
function graphemes(text) {
|
||||||
|
const groups = [];
|
||||||
|
for (const char of text) {
|
||||||
|
if (groups.length && (/\p{M}/u.test(char) || char === '\u200c' || char === '\u200d')) groups[groups.length - 1] += char;
|
||||||
|
else groups.push(char);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
function layoutText(value, anchor, width, caretIndex = value.length) {
|
||||||
|
const glyphs = [];
|
||||||
|
const rtl = bidi.getEmbeddingLevels(value).paragraphs[0]?.level % 2 === 1;
|
||||||
|
const available = Math.max(1, rtl ? anchor.x : width - anchor.x);
|
||||||
|
function editorLines(text) {
|
||||||
|
const result = [];
|
||||||
|
for (const paragraph of text.replace(/\t/g, ' ').split('\n')) {
|
||||||
|
let current = '';
|
||||||
|
for (const char of graphemes(paragraph)) {
|
||||||
|
if (current && line(current + char).width > available) { result.push(current); current = ''; }
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
result.push(current);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const lines = editorLines(value);
|
||||||
|
let top = anchor.y, caret = {x: anchor.x, y: top, height: 12};
|
||||||
|
const prefix = value.slice(0, caretIndex);
|
||||||
|
const before = editorLines(prefix);
|
||||||
|
lines.forEach((text, index) => {
|
||||||
|
const shaped = line(text);
|
||||||
|
let x = rtl ? anchor.x - shaped.width : anchor.x;
|
||||||
|
for (const placed of shaped.glyphs) glyphs.push({bitmap: placed.bitmap, x: x + placed.x, baseline: top + shaped.above + placed.y});
|
||||||
|
if (index === before.length - 1) {
|
||||||
|
const advance = line(before[index] || '').advance;
|
||||||
|
caret = {x: rtl ? anchor.x - advance : anchor.x + advance, y: top, height: shaped.height - 2};
|
||||||
|
}
|
||||||
|
top += shaped.height;
|
||||||
|
});
|
||||||
|
return {glyphs, caret, arabic: true};
|
||||||
|
}
|
||||||
|
function subtitleMask(value) {
|
||||||
|
const lines = wrap(value, 244);
|
||||||
|
if (lines.length > 2) throw new Error('Keep each subtitle to two lines. Split longer text into another subtitle.');
|
||||||
|
const shaped = lines.map(line), glyphs = [];
|
||||||
|
let top = 192 - 4 - shaped.reduce((sum, value) => sum + value.height, 0) + 1;
|
||||||
|
for (const value of shaped) {
|
||||||
|
let x = Math.floor((256 - value.width) / 2);
|
||||||
|
for (const placed of value.glyphs) glyphs.push({bitmap: placed.bitmap, x: x + placed.x, baseline: top + value.above + placed.y});
|
||||||
|
top += value.height;
|
||||||
|
}
|
||||||
|
return {mask: rasterize(glyphs, 256, 192), lines};
|
||||||
|
}
|
||||||
|
global.FlipnoteArabicText = {load, configure, contains, visual, line, layoutText, rasterize, subtitleMask};
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":2,"glyphs":{" ":{"name":"space","advance":5,"spans":[],"top":0,"bottom":0},"!":{"name":"exclam","advance":2,"spans":[[-9,0,1],[-8,0,1],[-7,0,1],[-6,0,1],[-5,0,1],[-4,0,1],[-2,0,1],[-1,0,1]],"top":-9,"bottom":0},"\"":{"name":"quotedbl","advance":6,"spans":[[-9,2,1],[-9,4,1],[-8,1,1],[-8,3,1],[-7,0,1],[-7,2,1]],"top":-9,"bottom":-6},"#":{"name":"numbersign","advance":8,"spans":[[-9,3,1],[-9,5,1],[-8,3,1],[-8,5,1],[-7,1,6],[-6,2,1],[-6,4,1],[-5,2,1],[-5,4,1],[-4,2,1],[-4,4,1],[-3,0,6],[-2,1,1],[-2,3,1],[-1,1,1],[-1,3,1]],"top":-9,"bottom":0},"%":{"name":"percent","advance":8,"spans":[[-9,1,1],[-9,5,1],[-8,0,1],[-8,2,1],[-8,5,1],[-7,0,1],[-7,2,1],[-7,4,1],[-6,1,1],[-6,4,1],[-5,3,1],[-4,2,1],[-4,5,1],[-3,2,1],[-3,4,1],[-3,6,1],[-2,1,1],[-2,4,1],[-2,6,1],[-1,1,1],[-1,5,1]],"top":-9,"bottom":0},"'":{"name":"quotesingle","advance":2,"spans":[[-9,0,1],[-8,0,1]],"top":-9,"bottom":-7},"(":{"name":"parenleft","advance":4,"spans":[[-9,2,1],[-8,1,1],[-7,1,1],[-6,0,1],[-5,0,1],[-4,0,1],[-3,1,1],[-2,1,1],[-1,2,1]],"top":-9,"bottom":0},")":{"name":"parenright","advance":4,"spans":[[-9,0,1],[-8,1,1],[-7,1,1],[-6,2,1],[-5,2,1],[-4,2,1],[-3,1,1],[-2,1,1],[-1,0,1]],"top":-9,"bottom":0},"*":{"name":"asterisk","advance":8,"spans":[[-8,3,1],[-7,0,1],[-7,3,1],[-7,6,1],[-6,1,1],[-6,3,1],[-6,5,1],[-5,2,3],[-4,1,1],[-4,3,1],[-4,5,1],[-3,0,1],[-3,3,1],[-3,6,1],[-2,3,1]],"top":-8,"bottom":-1},"+":{"name":"plus","advance":6,"spans":[[-7,2,1],[-6,2,1],[-5,0,5],[-4,2,1],[-3,2,1]],"top":-7,"bottom":-2},",":{"name":"comma","advance":3,"spans":[[-1,1,1],[0,0,1]],"top":-1,"bottom":1},"-":{"name":"hyphen","advance":6,"spans":[[-5,0,5]],"top":-5,"bottom":-4},".":{"name":"period","advance":2,"spans":[[-1,0,1]],"top":-1,"bottom":0},"/":{"name":"slash","advance":4,"spans":[[-9,2,1],[-8,2,1],[-7,2,1],[-6,1,1],[-5,1,1],[-4,1,1],[-3,0,1],[-2,0,1],[-1,0,1]],"top":-9,"bottom":0},"0":{"name":"zero","advance":6,"spans":[[-9,1,3],[-8,0,1],[-8,4,1],[-7,0,1],[-7,3,2],[-6,0,1],[-6,3,2],[-5,0,1],[-5,2,1],[-5,4,1],[-4,0,2],[-4,4,1],[-3,0,2],[-3,4,1],[-2,0,1],[-2,4,1],[-1,1,3]],"top":-9,"bottom":0},"1":{"name":"one","advance":3,"spans":[[-9,0,2],[-8,1,1],[-7,1,1],[-6,1,1],[-5,1,1],[-4,1,1],[-3,1,1],[-2,1,1],[-1,1,1]],"top":-9,"bottom":0},"2":{"name":"two","advance":6,"spans":[[-9,0,4],[-8,4,1],[-7,4,1],[-6,4,1],[-5,1,3],[-4,0,1],[-3,0,1],[-2,0,1],[-1,0,5]],"top":-9,"bottom":0},"3":{"name":"three","advance":6,"spans":[[-9,0,4],[-8,4,1],[-7,4,1],[-6,4,1],[-5,1,3],[-4,4,1],[-3,4,1],[-2,4,1],[-1,0,4]],"top":-9,"bottom":0},"4":{"name":"four","advance":7,"spans":[[-9,0,1],[-9,4,1],[-8,0,1],[-8,4,1],[-7,0,1],[-7,4,1],[-6,0,1],[-6,4,1],[-5,0,1],[-5,4,1],[-4,1,5],[-3,4,1],[-2,4,1],[-1,4,1]],"top":-9,"bottom":0},"5":{"name":"five","advance":6,"spans":[[-9,0,5],[-8,0,1],[-7,0,1],[-6,0,1],[-5,0,4],[-4,4,1],[-3,4,1],[-2,4,1],[-1,0,4]],"top":-9,"bottom":0},"6":{"name":"six","advance":6,"spans":[[-9,1,3],[-8,0,1],[-7,0,1],[-6,0,1],[-5,0,4],[-4,0,1],[-4,4,1],[-3,0,1],[-3,4,1],[-2,0,1],[-2,4,1],[-1,1,3]],"top":-9,"bottom":0},"7":{"name":"seven","advance":6,"spans":[[-9,0,5],[-8,4,1],[-7,4,1],[-6,3,1],[-5,2,1],[-4,2,1],[-3,2,1],[-2,2,1],[-1,2,1]],"top":-9,"bottom":0},"8":{"name":"eight","advance":6,"spans":[[-9,1,3],[-8,0,1],[-8,4,1],[-7,0,1],[-7,4,1],[-6,0,1],[-6,4,1],[-5,1,3],[-4,0,1],[-4,4,1],[-3,0,1],[-3,4,1],[-2,0,1],[-2,4,1],[-1,1,3]],"top":-9,"bottom":0},"9":{"name":"nine","advance":6,"spans":[[-9,1,3],[-8,0,1],[-8,4,1],[-7,0,1],[-7,4,1],[-6,0,1],[-6,4,1],[-5,1,4],[-4,4,1],[-3,4,1],[-2,4,1],[-1,1,3]],"top":-9,"bottom":0},":":{"name":"colon","advance":2,"spans":[[-7,0,1],[-3,0,1]],"top":-7,"bottom":-2},";":{"name":"semicolon","advance":3,"spans":[[-7,1,1],[-3,1,1],[-2,0,1]],"top":-7,"bottom":-1},"<":{"name":"less","advance":4,"spans":[[-7,2,1],[-6,1,1],[-5,0,1],[-4,1,1],[-3,2,1]],"top":-7,"bottom":-2},"=":{"name":"equal","advance":6,"spans":[[-6,0,5],[-4,0,5]],"top":-6,"bottom":-3},">":{"name":"greater","advance":4,"spans":[[-7,0,1],[-6,1,1],[-5,2,1],[-4,1,1],[-3,0,1]],"top":-7,"bottom":-2},"?":{"name":"question","advance":5,"spans":[[-9,0,3],[-8,3,1],[-7,3,1],[-6,2,1],[-5,1,1],[-4,1,1],[-2,1,1],[-1,1,1]],"top":-9,"bottom":0},"[":{"name":"bracketleft","advance":4,"spans":[[-9,0,3],[-8,0,1],[-7,0,1],[-6,0,1],[-5,0,1],[-4,0,1],[-3,0,1],[-2,0,1],[-1,0,3]],"top":-9,"bottom":0},"\\":{"name":"backslash","advance":4,"spans":[[-9,0,1],[-8,0,1],[-7,0,1],[-6,1,1],[-5,1,1],[-4,1,1],[-3,2,1],[-2,2,1],[-1,2,1]],"top":-9,"bottom":0},"]":{"name":"bracketright","advance":4,"spans":[[-9,0,3],[-8,2,1],[-7,2,1],[-6,2,1],[-5,2,1],[-4,2,1],[-3,2,1],[-2,2,1],[-1,0,3]],"top":-9,"bottom":0},"^":{"name":"asciicircum","advance":4,"spans":[[-9,1,1],[-8,0,1],[-8,2,1]],"top":-9,"bottom":-7},"_":{"name":"underscore","advance":6,"spans":[[-1,0,5]],"top":-1,"bottom":0},"{":{"name":"braceleft","advance":5,"spans":[[-9,2,2],[-8,1,1],[-7,1,1],[-6,1,1],[-5,0,1],[-4,1,1],[-3,1,1],[-2,1,1],[-1,2,2]],"top":-9,"bottom":0},"|":{"name":"bar","advance":2,"spans":[[-9,0,1],[-8,0,1],[-7,0,1],[-6,0,1],[-5,0Line truncated
|
||||||
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
Copyright (c) 2021 Jason Johnston
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).bidi_js=e()}(this,(function(){"use strict";return function(){return function(r){var e={R:"13k,1a,2,3,3,2+1j,ch+16,a+1,5+2,2+n,5,a,4,6+16,4+3,h+1b,4mo,179q,2+9,2+11,2i9+7y,2+68,4,3+4,5+13,4+3,2+4k,3+29,8+cf,1t+7z,w+17,3+3m,1t+3z,16o1+5r,8+30,8+mc,29+1r,29+4v,75+73",EN:"1c+9,3d+1,6,187+9,513,4+5,7+9,sf+j,175h+9,qw+q,161f+1d,4xt+a,25i+9",ES:"17,2,6dp+1,f+1,av,16vr,mx+1,4o,2",ET:"z+2,3h+3,b+1,ym,3e+1,2o,p4+1,8,6u,7c,g6,1wc,1n9+4,30+1b,2n,6d,qhx+1,h0m,a+1,49+2,63+1,4+1,6bb+3,12jj",AN:"16o+5,2j+9,2+1,35,ed,1ff2+9,87+u",CS:"18,2+1,b,2u,12k,55v,l,17v0,2,3,53,2+1,b",B:"a,3,f+2,2v,690",S:"9,2,k",WS:"c,k,4f4,1vk+a,u,1j,335",ON:"x+1,4+4,h+5,r+5,r+3,z,5+3,2+1,2+1,5,2+2,3+4,o,w,ci+1,8+d,3+d,6+8,2+g,39+1,9,6+1,2,33,b8,3+1,3c+1,7+1,5r,b,7h+3,sa+5,2,3i+6,jg+3,ur+9,2v,ij+1,9g+9,7+a,8m,4+1,49+x,14u,2+2,c+2,e+2,e+2,e+1,i+n,e+e,2+p,u+2,e+2,36+1,2+3,2+1,b,2+2,6+5,2,2,2,h+1,5+4,6+3,3+f,16+2,5+3l,3+81,1y+p,2+40,q+a,m+13,2r+ch,2+9e,75+hf,3+v,2+2w,6e+5,f+6,75+2a,1a+p,2+2g,d+5x,r+b,6+3,4+o,g,6+1,6+2,2k+1,4,2j,5h+z,1m+1,1e+f,t+2,1f+e,d+3,4o+3,2s+1,w,535+1r,h3l+1i,93+2,2s,b+1,3l+x,2v,4g+3,21+3,kz+1,g5v+1,5a,j+9,n+v,2,3,2+8,2+1,3+2,2,3,46+1,4+4,h+5,r+5,r+a,3h+2,4+6,b+4,78,1r+24,4+c,4,1hb,ey+6,103+j,16j+c,1ux+7,5+g,fsh,jdq+1t,4,57+2e,p1,1m,1m,1m,1m,4kt+1,7j+17,5+2r,d+e,3+e,2+e,2+10,m+4,w,1n+5,1q,4z+5,4b+rb,9+c,4+c,4+37,d+2g,8+b,l+b,5+1j,9+9,7+13,9+t,3+1,27+3c,2+29,2+3q,d+d,3+4,4+2,6+6,a+o,8+6,a+2,e+6,16+42,2+1i",BN:"0+8,6+d,2s+5,2+p,e,4m9,1kt+2,2b+5,5+5,17q9+v,7k,6p+8,6+1,119d+3,440+7,96s+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+1,1ekf+75,6p+2rz,1ben+1,1ekf+1,1ekf+1",NSM:"lc+33,7o+6,7c+18,2,2+1,2+1,2,21+a,1d+k,h,2u+6,3+5,3+1,2+3,10,v+q,2k+a,1n+8,a,p+3,2+8,2+2,2+4,18+2,3c+e,2+v,1k,2,5+7,5,4+6,b+1,u,1n,5+3,9,l+1,r,3+1,1m,5+1,5+1,3+2,4,v+1,4,c+1,1m,5+4,2+1,5,l+1,n+5,2,1n,3,2+3,9,8+1,c+1,v,1q,d,1f,4,1m+2,6+2,2+3,8+1,c+1,u,1n,g+1,l+1,t+1,1m+1,5+3,9,l+1,u,21,8+2,2,2j,3+6,d+7,2r,3+8,c+5,23+1,s,2,2,1k+d,2+4,2+1,6+a,2+z,a,2v+3,2+5,2+1,3+1,q+1,5+2,h+3,e,3+1,7,g,jk+2,qb+2,u+2,u+1,v+1,1t+1,2+6,9,3+a,a,1a+2,3c+1,z,3b+2,5+1,a,7+2,64+1,3,1n,2+6,2,2,3+7,7+9,3,1d+g,1s+3,1d,2+4,2,6,15+8,d+1,x+3,3+1,2+2,1l,2+1,4,2+2,1n+7,3+1,49+2,2+c,2+6,5,7,4+1,5j+1l,2+4,k1+w,2db+2,3y,2p+v,ff+3,30+1,n9x+3,2+9,x+1,29+1,7l,4,5,q+1,6,48+1,r+h,e,13+7,q+a,1b+2,1d,3+3,3+1,14,1w+5,3+1,3+1,d,9,1c,1g,2+2,3+1,6+1,2,17+1,9,6n,3,5,fn5,ki+f,h+f,r2,6b,46+4,1af+2,2+1,6+3,15+2,5,4m+1,fy+3,as+1,4a+a,4x,1j+e,1l+2,1e+3,3+1,1y+2,11+4,2+7,1r,d+1,1h+8,b+3,3,2o+2,3,2+1,7,4h,4+7,m+1,1m+1,4,12+6,4+4,5g+7,3+2,2,o,2d+5,2,5+1,2+1,6n+3,7+1,2+1,s+1,2e+7,3,2+1,2z,2,3+5,2,2u+2,3+3,2+4,78+8,2+1,75+1,2,5,41+3,3+1,5,x+5,3+1,15+5,3+3,9,a+5,3+2,1b+c,2+1,bb+6,2+5,2d+l,3+6,2+1,2+1,3f+5,4,2+1,2+6,2,21+1,4,2,9o+1,f0c+4,1o+6,t5,1s+3,2a,f5l+1,43t+2,i+7,3+6,v+3,45+2,1j0+1i,5+1d,9,f,n+4,2+e,11t+6,2+g,3+6,2+1,2+4,7a+6,c6+3,15t+6,32+6,gzhy+6n",AL:"16w,3,2,e+1b,z+2,2+2s,g+1,8+1,b+m,2+t,s+2i,c+e,4h+f,1d+1e,1bwe+dp,3+3z,x+c,2+1,35+3y,2rm+z,5+7,b+5,dt+l,c+u,17nl+27,1t+27,4x+6n,3+d",LRO:"6ct",RLO:"6cu",LRE:"6cq",RLE:"6cr",PDF:"6cs",LRI:"6ee",RLI:"6ef",FSI:"6eg",PDI:"6eh"},f={},a={};f.L=1,a[1]="L",Object.keys(e).forEach((function(r,e){f[r]=1<<e+1,a[f[r]]=r})),Object.freeze(f);var n=f.LRI|f.RLI|f.FSI,i=f.L|f.R|f.AL,v=f.B|f.S|f.WS|f.ON|f.FSI|f.LRI|f.RLI|f.PDI,o=f.BN|f.RLE|f.LRE|f.RLO|f.LRO|f.PDF,t=f.S|f.WS|f.B|n|f.PDI|o,u=null;function l(r){return function(){if(!u){u=new Map;var r=function(r){if(e.hasOwnProperty(r)){var a=0;e[r].split(",").forEach((function(e){var n=e.split("+"),i=n[0],v=n[1];i=parseInt(i,36),v=v?parseInt(v,36):0,u.set(a+=i,f[r]);for(var o=0;o<v;o++)u.set(++a,f[r])}))}};for(var a in e)r(a)}}(),u.get(r.codePointAt(0))||f.L}var c,d,b,s="14>1,1e>2,u>2,2wt>1,1>1,1ge>1,1wp>1,1j>1,f>1,hm>1,1>1,u>1,u6>1,1>1,+5,28>1,w>1,1>1,+3,b8>1,1>1,+3,1>3,-1>-1,3>1,1>1,+2,1s>1,1>1,x>1,th>1,1>1,+2,db>1,1>1,+3,3>1,1>1,+2,14qm>1,1>1,+1,4q>1,1e>2,u>2,2>1,+1",h="6f1>-6dx,6dy>-6dx,6ec>-6ed,6ee>-6ed,6ww>2jj,-2ji>2jj,14r4>-1e7l,1e7m>-1e7l,1e7m>-1e5c,1e5d>-1e5b,1e5c>-14qx,14qy>-14qx,14vn>-1ecg,1ech>-1ecg,1edu>-1ecg,1eci>-1ecg,1eda>-1ecg,1eci>-1ecg,1eci>-168q,168r>-168q,168s>-14ye,14yf>-14ye";function k(r,e){var f,a=0,n=new Map,i=e&&new Map;return r.split(",").forEach((function r(v){if(-1!==v.indexOf("+"))for(var o=+v;o--;)r(f);else{f=v;var t=v.split(">"),u=t[0],l=t[1];u=String.fromCodePoint(a+=parseInt(u,36)),l=String.fromCodePoint(a+=parseInt(l,36)),n.set(u,l),e&&i.set(l,u)}})),{map:n,reverseMap:i}}function m(){if(!c){var r=k(s,!0),e=r.map,f=r.reverseMap;c=e,d=f,b=k(h,!1).map}}function j(r){return m(),c.get(r)||null}function g(r){return m(),d.get(r)||null}function p(r){return m(),b.get(r)||null}var q=f.L,w=f.R,y=f.EN,x=f.ES,_=f.ET,M=f.AN,z=f.CS,I=f.B,L=f.S,S=f.ON,R=f.BN,O=f.NSM,E=f.AL,N=f.LRO,T=f.RLO,A=f.LRE,D=f.RLE,P=f.PDF,W=f.LRI,B=f.RLI,F=f.FSI,U=f.PDI;var C;function G(r){return function(){if(!C){var r=k("14>1,j>Line truncated
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,661 @@
|
|||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WIDTH = 256;
|
||||||
|
const HEIGHT = 192;
|
||||||
|
const RNG_INITIAL_STATE = 0x1F01;
|
||||||
|
|
||||||
|
const PEN_NAMES = [
|
||||||
|
'1 px',
|
||||||
|
'Direction-aware 2 px',
|
||||||
|
'3×3 square',
|
||||||
|
'Squiggle',
|
||||||
|
'Directional =',
|
||||||
|
'Sparse airbrush',
|
||||||
|
'Jitter line',
|
||||||
|
'Dense spray',
|
||||||
|
'5×5 square',
|
||||||
|
'7×7 square',
|
||||||
|
'9×9 square',
|
||||||
|
'Dynamic-width pen',
|
||||||
|
'Hollow diamond',
|
||||||
|
'Large airbrush',
|
||||||
|
'Wide jitter line',
|
||||||
|
'Heavy spray',
|
||||||
|
];
|
||||||
|
|
||||||
|
const PAINT_NAMES = [
|
||||||
|
'Sparse 3×3 dots',
|
||||||
|
'Sparse 2×2 dots',
|
||||||
|
'Vertical stripes',
|
||||||
|
'Horizontal stripes',
|
||||||
|
'Checkerboard',
|
||||||
|
'Dense 2×2',
|
||||||
|
'Dense 3×3',
|
||||||
|
'Custom motif',
|
||||||
|
];
|
||||||
|
|
||||||
|
const FILL_NAMES = [
|
||||||
|
'Sparse 3×3 fill',
|
||||||
|
'Sparse 2×2 fill',
|
||||||
|
'Vertical stripe fill',
|
||||||
|
'Horizontal stripe fill',
|
||||||
|
'Checkerboard fill',
|
||||||
|
'Dense 2×2 fill',
|
||||||
|
'Dense 3×3 fill',
|
||||||
|
'Solid fill',
|
||||||
|
];
|
||||||
|
|
||||||
|
// The supplied ROM notes identify all eight eraser action slots and state
|
||||||
|
// that advanced erasing keeps the base geometry while filtering candidate
|
||||||
|
// pixels, but do not include the exact geometry/filter routines. These
|
||||||
|
// names/geometries preserve the restored editor's existing DSi palette
|
||||||
|
// until those remaining routines are ported from the ROM.
|
||||||
|
const ERASER_NAMES = [
|
||||||
|
'Pixel',
|
||||||
|
'3×3 square',
|
||||||
|
'Medium round',
|
||||||
|
'Large round',
|
||||||
|
'9×9 square',
|
||||||
|
'15×15 square',
|
||||||
|
'Vertical bar',
|
||||||
|
'Horizontal bar',
|
||||||
|
];
|
||||||
|
|
||||||
|
const AIRBRUSH_SMALL_MASK = [
|
||||||
|
'..#..',
|
||||||
|
'.###.',
|
||||||
|
'#####',
|
||||||
|
'.###.',
|
||||||
|
'..#..',
|
||||||
|
];
|
||||||
|
|
||||||
|
const AIRBRUSH_LARGE_MASK = [
|
||||||
|
'......###......',
|
||||||
|
'.....#####.....',
|
||||||
|
'...#########...',
|
||||||
|
'..###########..',
|
||||||
|
'.#############.',
|
||||||
|
'###############',
|
||||||
|
'###############',
|
||||||
|
'###############',
|
||||||
|
'###############',
|
||||||
|
'###############',
|
||||||
|
'.#############.',
|
||||||
|
'..###########..',
|
||||||
|
'...#########...',
|
||||||
|
'.....#####.....',
|
||||||
|
'......###......',
|
||||||
|
];
|
||||||
|
|
||||||
|
const RING_OUTER_MASK = [
|
||||||
|
'..###..',
|
||||||
|
'.#####.',
|
||||||
|
'#######',
|
||||||
|
'#######',
|
||||||
|
'#######',
|
||||||
|
'.#####.',
|
||||||
|
'..###..',
|
||||||
|
];
|
||||||
|
|
||||||
|
const RING_INNER_MASK = [
|
||||||
|
'..#..',
|
||||||
|
'.###.',
|
||||||
|
'#####',
|
||||||
|
'.###.',
|
||||||
|
'..#..',
|
||||||
|
];
|
||||||
|
|
||||||
|
let rngState = RNG_INITIAL_STATE;
|
||||||
|
|
||||||
|
function resetRandom(state) {
|
||||||
|
const next = Number(state);
|
||||||
|
rngState = Number.isFinite(next) ? (next & 0xffff) : RNG_INITIAL_STATE;
|
||||||
|
if (!rngState) rngState = RNG_INITIAL_STATE;
|
||||||
|
return rngState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomState() {
|
||||||
|
return rngState & 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function random16() {
|
||||||
|
const old = rngState & 0xffff;
|
||||||
|
const bit0 = old & 1;
|
||||||
|
const bit3 = (old >> 3) & 1;
|
||||||
|
rngState = old >> 1;
|
||||||
|
if (bit0 === bit3) rngState |= 0x8000;
|
||||||
|
rngState &= 0xffff;
|
||||||
|
return rngState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomRange(range) {
|
||||||
|
const safeRange = Math.max(1, Math.floor(range));
|
||||||
|
return random16() % safeRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function inBounds(x, y) {
|
||||||
|
return x >= 0 && y >= 0 && x < WIDTH && y < HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexOf(x, y) {
|
||||||
|
return y * WIDTH + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePixel(mask, x, y, value) {
|
||||||
|
if (!inBounds(x, y)) return;
|
||||||
|
mask[indexOf(x, y)] = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPixel(mask, x, y) {
|
||||||
|
writePixel(mask, x, y, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPixel(mask, x, y) {
|
||||||
|
writePixel(mask, x, y, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineSteps(x0, y0, x1, y1, includeStart) {
|
||||||
|
let x = Math.round(x0);
|
||||||
|
let y = Math.round(y0);
|
||||||
|
const tx = Math.round(x1);
|
||||||
|
const ty = Math.round(y1);
|
||||||
|
const points = [];
|
||||||
|
const dx = Math.abs(tx - x);
|
||||||
|
const sx = x < tx ? 1 : -1;
|
||||||
|
const dyAbs = Math.abs(ty - y);
|
||||||
|
const sy = y < ty ? 1 : -1;
|
||||||
|
let err = dx - dyAbs;
|
||||||
|
let first = true;
|
||||||
|
let previousX = x;
|
||||||
|
let previousY = y;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (includeStart || !first) {
|
||||||
|
points.push({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
sx: first ? 0 : x - previousX,
|
||||||
|
sy: first ? 0 : y - previousY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (x === tx && y === ty) break;
|
||||||
|
first = false;
|
||||||
|
previousX = x;
|
||||||
|
previousY = y;
|
||||||
|
const e2 = 2 * err;
|
||||||
|
if (e2 > -dyAbs) {
|
||||||
|
err -= dyAbs;
|
||||||
|
x += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
y += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampSquare(mask, x, y, radius, value) {
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
writePixel(mask, x + dx, y + dy, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampMask(mask, rows, x, y, value) {
|
||||||
|
const height = rows.length;
|
||||||
|
const width = rows[0].length;
|
||||||
|
const ox = Math.floor(width / 2);
|
||||||
|
const oy = Math.floor(height / 2);
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
if (rows[row][col] === '#') writePixel(mask, x + col - ox, y + row - oy, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampDirectionAware(mask, point, value) {
|
||||||
|
const x = point.x;
|
||||||
|
const y = point.y;
|
||||||
|
const sx = point.sx || 0;
|
||||||
|
const sy = point.sy || 0;
|
||||||
|
writePixel(mask, x, y, value);
|
||||||
|
writePixel(mask, x + sy, y, value);
|
||||||
|
writePixel(mask, x, y + sx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampDirectionalEqual(mask, point) {
|
||||||
|
const x = point.x;
|
||||||
|
const y = point.y;
|
||||||
|
const sx = point.sx || 0;
|
||||||
|
const sy = point.sy || 0;
|
||||||
|
const horizontalMotion = sx !== 0 && sy === 0;
|
||||||
|
const verticalMotion = sy !== 0 && sx === 0;
|
||||||
|
|
||||||
|
if (horizontalMotion) {
|
||||||
|
setPixel(mask, x, y - 1);
|
||||||
|
setPixel(mask, x, y + 1);
|
||||||
|
} else if (verticalMotion) {
|
||||||
|
setPixel(mask, x - 1, y);
|
||||||
|
setPixel(mask, x + 1, y);
|
||||||
|
} else if ((sx === sy) || (sx === 0 && sy === 0)) {
|
||||||
|
setPixel(mask, x - 1, y);
|
||||||
|
setPixel(mask, x + 1, y);
|
||||||
|
} else {
|
||||||
|
setPixel(mask, x, y - 1);
|
||||||
|
setPixel(mask, x, y + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPixel(mask, x, y);
|
||||||
|
if (Math.abs(sx) >= Math.abs(sy)) setPixel(mask, x, y + (sx || 1));
|
||||||
|
else setPixel(mask, x + (sy || 1), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampSquiggle(mask, point, state) {
|
||||||
|
const qx = point.x & ~1;
|
||||||
|
const qy = point.y & ~1;
|
||||||
|
const anchor = state.squiggleAnchor;
|
||||||
|
if (anchor && (anchor.x !== qx || anchor.y !== qy)) {
|
||||||
|
const mx = ((anchor.x + qx) >> 1) & ~1;
|
||||||
|
const my = ((anchor.y + qy) >> 1) & ~1;
|
||||||
|
stampSquare(mask, mx, my, 0, 1);
|
||||||
|
setPixel(mask, mx + 1, my);
|
||||||
|
setPixel(mask, mx, my + 1);
|
||||||
|
setPixel(mask, mx + 1, my + 1);
|
||||||
|
}
|
||||||
|
setPixel(mask, qx, qy);
|
||||||
|
setPixel(mask, qx + 1, qy);
|
||||||
|
setPixel(mask, qx, qy + 1);
|
||||||
|
setPixel(mask, qx + 1, qy + 1);
|
||||||
|
state.squiggleAnchor = {x: qx, y: qy};
|
||||||
|
}
|
||||||
|
|
||||||
|
function airbrushCandidatePass(segmentMagnitude, large) {
|
||||||
|
// The ROM consumes the LFSR while walking candidate pixels and folds
|
||||||
|
// segment delta into its skip logic. The supplied notes do not include
|
||||||
|
// the exact branch constants, so preserve both deterministic LFSR call
|
||||||
|
// order and motion sensitivity here pending the direct routine port.
|
||||||
|
const base = large ? 72 : 98;
|
||||||
|
const motionPenalty = Math.min(38, Math.max(0, segmentMagnitude - 1) * (large ? 2 : 4));
|
||||||
|
return (random16() & 0xff) < Math.max(24, base - motionPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampStochasticMask(mask, rows, point, segmentMagnitude) {
|
||||||
|
const height = rows.length;
|
||||||
|
const width = rows[0].length;
|
||||||
|
const ox = Math.floor(width / 2);
|
||||||
|
const oy = Math.floor(height / 2);
|
||||||
|
const large = width > 5;
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
if (rows[row][col] !== '#') continue;
|
||||||
|
if (!airbrushCandidatePass(segmentMagnitude, large)) continue;
|
||||||
|
setPixel(mask, point.x + col - ox, point.y + row - oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomOffset(mode) {
|
||||||
|
return mode ? randomRange(10) - 5 : randomRange(5) - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOnePixelLine(mask, x0, y0, x1, y1) {
|
||||||
|
for (const point of lineSteps(x0, y0, x1, y1, true)) setPixel(mask, point.x, point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawJitterSegment(mask, x0, y0, x1, y1, mode) {
|
||||||
|
const ax = clamp(Math.round(x0) + randomOffset(mode), 0, WIDTH - 1);
|
||||||
|
const ay = clamp(Math.round(y0) + randomOffset(mode), 0, HEIGHT - 1);
|
||||||
|
const bx = clamp(Math.round(x1) + randomOffset(mode), 0, WIDTH - 1);
|
||||||
|
const by = clamp(Math.round(y1) + randomOffset(mode), 0, HEIGHT - 1);
|
||||||
|
drawOnePixelLine(mask, ax, ay, bx, by);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sprayClass(dx, dy, auxiliaryValue) {
|
||||||
|
const dynamic = Number(auxiliaryValue) || 0;
|
||||||
|
if (dynamic) return clamp(Math.ceil(Math.abs(dynamic) / 120), 1, 6);
|
||||||
|
const d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 < 4) return 6;
|
||||||
|
if (d2 < 8) return 5;
|
||||||
|
if (d2 < 12) return 4;
|
||||||
|
if (d2 < 24) return 3;
|
||||||
|
if (d2 < 48) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawComplexSpray(mask, x0, y0, x1, y1, mode, auxiliaryValue) {
|
||||||
|
const dx = x1 - x0;
|
||||||
|
const dy = y1 - y0;
|
||||||
|
const cls = sprayClass(dx, dy, auxiliaryValue);
|
||||||
|
const count = cls + (mode ? 3 : 2);
|
||||||
|
const spread = (7 - cls) + (mode ? 5 : 2);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const scale = mode && (i & 1) ? 2 : 1;
|
||||||
|
const ax = clamp(Math.round(x0) + (randomRange(spread * 2 + 1) - spread) * scale, 0, WIDTH - 1);
|
||||||
|
const ay = clamp(Math.round(y0) + (randomRange(spread * 2 + 1) - spread) * scale, 0, HEIGHT - 1);
|
||||||
|
const bx = clamp(Math.round(x1) + (randomRange(spread * 2 + 1) - spread) * scale, 0, WIDTH - 1);
|
||||||
|
const by = clamp(Math.round(y1) + (randomRange(spread * 2 + 1) - spread) * scale, 0, HEIGHT - 1);
|
||||||
|
drawOnePixelLine(mask, ax, ay, bx, by);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintPattern(id, x, y, motif) {
|
||||||
|
switch (id & 7) {
|
||||||
|
case 0:
|
||||||
|
return x % 3 === 1 && y % 3 === 1;
|
||||||
|
case 1:
|
||||||
|
return (x & 1) === 0 && (y & 1) === 0;
|
||||||
|
case 2:
|
||||||
|
return (x & 1) === 1;
|
||||||
|
case 3:
|
||||||
|
return (y & 1) === 1;
|
||||||
|
case 4:
|
||||||
|
return ((x + y) & 1) === 1;
|
||||||
|
case 5:
|
||||||
|
return (x & 1) !== 0 || (y & 1) !== 0;
|
||||||
|
case 6:
|
||||||
|
return !(x % 3 === 1 && y % 3 === 1);
|
||||||
|
case 7:
|
||||||
|
if (motif && motif.data && motif.width && motif.height) {
|
||||||
|
const mx = ((x % motif.width) + motif.width) % motif.width;
|
||||||
|
const my = ((y % motif.height) + motif.height) % motif.height;
|
||||||
|
return !!motif.data[my * motif.width + mx];
|
||||||
|
}
|
||||||
|
// The supplied excerpt identifies slot 7 as a separate action
|
||||||
|
// but does not include its editor/capture routine. Keep the
|
||||||
|
// restored editor's neutral criss-cross motif until that ROM
|
||||||
|
// routine is supplied/ported.
|
||||||
|
return ((x - y) % 5 === 0) || ((x + y) % 5 === 0);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampPaint(mask, logicalId, x, y, options) {
|
||||||
|
const baseId = logicalId & 7;
|
||||||
|
const expert = logicalId >= 8;
|
||||||
|
const paintState = Number(options && options.paintState);
|
||||||
|
const radius = paintState === 2 ? 3 : (expert ? 7 : 1);
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
const px = x + dx;
|
||||||
|
const py = y + dy;
|
||||||
|
if (!inBounds(px, py)) continue;
|
||||||
|
if (paintPattern(baseId, px, py, options && options.motif)) setPixel(mask, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Bucket-style patterned fill. The connected transparent region is found
|
||||||
|
// from the pre-fill layer state so setting patterned pixels cannot split
|
||||||
|
// the traversal while it runs. Slots 0-6 reuse Paint 1-7's absolute-canvas
|
||||||
|
// predicates. Slot 7 is deliberately solid, per the editor UI spec.
|
||||||
|
function floodFillPattern(mask, slot, startX, startY, options) {
|
||||||
|
const sx = Math.round(startX);
|
||||||
|
const sy = Math.round(startY);
|
||||||
|
if (!inBounds(sx, sy)) return 0;
|
||||||
|
const startIndex = sy * WIDTH + sx;
|
||||||
|
// Normally the current layer is both the destination and the fill
|
||||||
|
// boundary. Shift-fill can supply the other layer as boundaryMask so
|
||||||
|
// its ink contains the flood while the current layer still receives
|
||||||
|
// the selected fill pattern/colour.
|
||||||
|
const suppliedBoundary = options && options.boundaryMask;
|
||||||
|
const boundaryMask = suppliedBoundary && suppliedBoundary.length === mask.length
|
||||||
|
? suppliedBoundary
|
||||||
|
: mask;
|
||||||
|
if (boundaryMask[startIndex]) return 0;
|
||||||
|
|
||||||
|
const original = new Uint8Array(boundaryMask);
|
||||||
|
const visited = new Uint8Array(WIDTH * HEIGHT);
|
||||||
|
const queueX = new Int16Array(WIDTH * HEIGHT);
|
||||||
|
const queueY = new Int16Array(WIDTH * HEIGHT);
|
||||||
|
let head = 0;
|
||||||
|
let tail = 0;
|
||||||
|
let changed = 0;
|
||||||
|
queueX[tail] = sx;
|
||||||
|
queueY[tail] = sy;
|
||||||
|
tail++;
|
||||||
|
visited[startIndex] = 1;
|
||||||
|
|
||||||
|
const patternId = clamp(Math.round(Number(slot) || 0), 0, 7);
|
||||||
|
const solid = patternId === 7;
|
||||||
|
const motif = options && options.motif;
|
||||||
|
|
||||||
|
while (head < tail) {
|
||||||
|
const x = queueX[head];
|
||||||
|
const y = queueY[head];
|
||||||
|
head++;
|
||||||
|
const index = y * WIDTH + x;
|
||||||
|
|
||||||
|
if (solid || paintPattern(patternId, x, y, motif)) {
|
||||||
|
if (!mask[index]) {
|
||||||
|
mask[index] = 1;
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x > 0) {
|
||||||
|
const n = index - 1;
|
||||||
|
if (!visited[n] && !original[n]) {
|
||||||
|
visited[n] = 1;
|
||||||
|
queueX[tail] = x - 1;
|
||||||
|
queueY[tail] = y;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x + 1 < WIDTH) {
|
||||||
|
const n = index + 1;
|
||||||
|
if (!visited[n] && !original[n]) {
|
||||||
|
visited[n] = 1;
|
||||||
|
queueX[tail] = x + 1;
|
||||||
|
queueY[tail] = y;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y > 0) {
|
||||||
|
const n = index - WIDTH;
|
||||||
|
if (!visited[n] && !original[n]) {
|
||||||
|
visited[n] = 1;
|
||||||
|
queueX[tail] = x;
|
||||||
|
queueY[tail] = y - 1;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y + 1 < HEIGHT) {
|
||||||
|
const n = index + WIDTH;
|
||||||
|
if (!visited[n] && !original[n]) {
|
||||||
|
visited[n] = 1;
|
||||||
|
queueX[tail] = x;
|
||||||
|
queueY[tail] = y + 1;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraserContains(slot, dx, dy) {
|
||||||
|
switch (slot & 7) {
|
||||||
|
case 0: return dx === 0 && dy === 0;
|
||||||
|
case 1: return Math.abs(dx) <= 1 && Math.abs(dy) <= 1;
|
||||||
|
case 2: return dx * dx + dy * dy <= 12;
|
||||||
|
case 3: return dx * dx + dy * dy <= 42;
|
||||||
|
case 4: return Math.abs(dx) <= 4 && Math.abs(dy) <= 4;
|
||||||
|
case 5: return Math.abs(dx) <= 7 && Math.abs(dy) <= 7;
|
||||||
|
case 6: return Math.abs(dx) <= 1 && Math.abs(dy) <= 6;
|
||||||
|
case 7: return Math.abs(dy) <= 1 && Math.abs(dx) <= 6;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraserRadius(slot) {
|
||||||
|
return [0, 1, 3, 6, 4, 7, 6, 6][slot & 7];
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampEraser(mask, logicalId, x, y, options) {
|
||||||
|
const slot = logicalId & 7;
|
||||||
|
const expert = logicalId >= 8;
|
||||||
|
const radius = eraserRadius(slot);
|
||||||
|
// Advanced Erase keeps the base Eraser geometry, but its candidate
|
||||||
|
// pixels use the most recently used Paint texture. Paint 5
|
||||||
|
// (checkerboard / pattern id 4) is the fallback until Paint has
|
||||||
|
// actually been used or selected in this editor session.
|
||||||
|
const requestedPattern = Number(options && options.eraserPatternId);
|
||||||
|
const eraserPatternId = Number.isFinite(requestedPattern)
|
||||||
|
? clamp(Math.round(requestedPattern), 0, 7)
|
||||||
|
: 4;
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
if (!eraserContains(slot, dx, dy)) continue;
|
||||||
|
const px = x + dx;
|
||||||
|
const py = y + dy;
|
||||||
|
if (expert && !paintPattern(eraserPatternId, px, py, options && options.motif)) continue;
|
||||||
|
clearPixel(mask, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStrokeState() {
|
||||||
|
return {
|
||||||
|
squiggleAnchor: null,
|
||||||
|
ringPrevious: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampPenPoint(mask, logicalId, point, state, segmentMagnitude, options) {
|
||||||
|
switch (logicalId) {
|
||||||
|
case 0:
|
||||||
|
setPixel(mask, point.x, point.y);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
stampDirectionAware(mask, point, 1);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
stampSquare(mask, point.x, point.y, 1, 1);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
stampSquiggle(mask, point, state);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
stampDirectionalEqual(mask, point);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
stampStochasticMask(mask, AIRBRUSH_SMALL_MASK, point, segmentMagnitude);
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
stampSquare(mask, point.x, point.y, 2, 1);
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
stampSquare(mask, point.x, point.y, 3, 1);
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
stampSquare(mask, point.x, point.y, 4, 1);
|
||||||
|
break;
|
||||||
|
case 11: {
|
||||||
|
const aux = Number(options && options.auxiliaryValue) || 0;
|
||||||
|
const d2 = segmentMagnitude * segmentMagnitude;
|
||||||
|
let cls;
|
||||||
|
if (aux) cls = clamp(Math.ceil(Math.abs(aux) / 120), 1, 3);
|
||||||
|
else if (d2 < 4) cls = 3;
|
||||||
|
else if (d2 < 32) cls = 2;
|
||||||
|
else cls = 1;
|
||||||
|
if (cls === 1) setPixel(mask, point.x, point.y);
|
||||||
|
else if (cls === 2) stampDirectionAware(mask, point, 1);
|
||||||
|
else stampSquare(mask, point.x, point.y, 1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 12: {
|
||||||
|
const previous = state.ringPrevious || {x: point.x, y: point.y};
|
||||||
|
stampMask(mask, RING_OUTER_MASK, point.x, point.y, 1);
|
||||||
|
stampMask(mask, RING_INNER_MASK, previous.x, previous.y, 0);
|
||||||
|
state.ringPrevious = {x: point.x, y: point.y};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 13:
|
||||||
|
stampStochasticMask(mask, AIRBRUSH_LARGE_MASK, point, segmentMagnitude);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setPixel(mask, point.x, point.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStrokeStart(mask, kind, logicalId, x, y, state, options) {
|
||||||
|
const point = {x: Math.round(x), y: Math.round(y), sx: 0, sy: 0};
|
||||||
|
const strokeState = state || createStrokeState();
|
||||||
|
if (kind === 'paint') stampPaint(mask, logicalId, point.x, point.y, options);
|
||||||
|
else if (kind === 'eraser') stampEraser(mask, logicalId, point.x, point.y, options);
|
||||||
|
else if (logicalId === 6 || logicalId === 14) {
|
||||||
|
setPixel(mask, point.x, point.y);
|
||||||
|
} else if (logicalId === 7 || logicalId === 15) {
|
||||||
|
setPixel(mask, point.x, point.y);
|
||||||
|
} else {
|
||||||
|
stampPenPoint(mask, logicalId, point, strokeState, 0, options);
|
||||||
|
}
|
||||||
|
return strokeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStrokeSegment(mask, kind, logicalId, x0, y0, x1, y1, state, options) {
|
||||||
|
const strokeState = state || createStrokeState();
|
||||||
|
const dx = Math.round(x1) - Math.round(x0);
|
||||||
|
const dy = Math.round(y1) - Math.round(y0);
|
||||||
|
const segmentMagnitude = Math.max(Math.abs(dx), Math.abs(dy));
|
||||||
|
|
||||||
|
if (kind === 'pen' && (logicalId === 6 || logicalId === 14)) {
|
||||||
|
drawJitterSegment(mask, x0, y0, x1, y1, logicalId === 14 ? 1 : 0);
|
||||||
|
return strokeState;
|
||||||
|
}
|
||||||
|
if (kind === 'pen' && (logicalId === 7 || logicalId === 15)) {
|
||||||
|
drawComplexSpray(mask, x0, y0, x1, y1, logicalId === 15 ? 1 : 0, options && options.auxiliaryValue);
|
||||||
|
return strokeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = lineSteps(x0, y0, x1, y1, false);
|
||||||
|
for (const point of points) {
|
||||||
|
if (kind === 'paint') stampPaint(mask, logicalId, point.x, point.y, options);
|
||||||
|
else if (kind === 'eraser') stampEraser(mask, logicalId, point.x, point.y, options);
|
||||||
|
else stampPenPoint(mask, logicalId, point, strokeState, segmentMagnitude, options);
|
||||||
|
}
|
||||||
|
return strokeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logicalId(slot, expert) {
|
||||||
|
return clamp(Math.round(Number(slot) || 0), 0, 7) + (expert ? 8 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolName(kind, logical) {
|
||||||
|
const id = clamp(Math.round(Number(logical) || 0), 0, 15);
|
||||||
|
if (kind === 'paint') return PAINT_NAMES[id & 7];
|
||||||
|
if (kind === 'fill') return FILL_NAMES[id & 7];
|
||||||
|
if (kind === 'eraser') return ERASER_NAMES[id & 7];
|
||||||
|
return PEN_NAMES[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
global.FlipnoteDrawingEngine = Object.freeze({
|
||||||
|
WIDTH,
|
||||||
|
HEIGHT,
|
||||||
|
RNG_INITIAL_STATE,
|
||||||
|
PEN_NAMES: PEN_NAMES.slice(),
|
||||||
|
PAINT_NAMES: PAINT_NAMES.slice(),
|
||||||
|
FILL_NAMES: FILL_NAMES.slice(),
|
||||||
|
ERASER_NAMES: ERASER_NAMES.slice(),
|
||||||
|
resetRandom,
|
||||||
|
getRandomState,
|
||||||
|
random16,
|
||||||
|
lineSteps,
|
||||||
|
paintPattern,
|
||||||
|
floodFillPattern,
|
||||||
|
logicalId,
|
||||||
|
toolName,
|
||||||
|
createStrokeState,
|
||||||
|
applyStrokeStart,
|
||||||
|
applyStrokeSegment,
|
||||||
|
});
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 793 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 818 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 876 B |
|
After Width: | Height: | Size: 907 B |
|
After Width: | Height: | Size: 793 B |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 797 B |
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 775 B |
|
After Width: | Height: | Size: 824 B |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 793 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 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: 1.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 161 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 813 B |
|
After Width: | Height: | Size: 811 B |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 781 B |
|
After Width: | Height: | Size: 781 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 799 B |
|
After Width: | Height: | Size: 836 B |
|
After Width: | Height: | Size: 860 B |
