Auto-format code with VS Code

Format JS and HTML using VS Code extension: https://marketplace.visualstudio.com/items?itemName=jbockle.jbockle-format-files

Dedented some files for now to make the diff intelligible:
- tools.js
- menus.js
- speech-recognition.js
- simulate-random-gestures.js
main
Isaiah Odhner 2022-01-18 13:33:44 -05:00
parent 5f64f1c251
commit 8053b38e58
37 changed files with 2692 additions and 2633 deletions

View File

@ -191,7 +191,7 @@ module.exports = {
"no-label-var": "error", "no-label-var": "error",
// "no-shadow": "error", // "no-shadow": "error",
// "no-use-before-define": "error", // "no-use-before-define": "error",
// To target specific variables to rename or otherwise address: // To target specific variables to rename or otherwise address:
"no-restricted-globals": ["error", "event", "canvas", "ctx", "colors", "i", "j", "k", "x", "y", "z", "width", "height", "w", "h"], "no-restricted-globals": ["error", "event", "canvas", "ctx", "colors", "i", "j", "k", "x", "y", "z", "width", "height", "w", "h"],

View File

@ -9,7 +9,7 @@
// @noframes // @noframes
// ==/UserScript== // ==/UserScript==
(function() { (function () {
'use strict'; 'use strict';
let cleanUp = null; let cleanUp = null;
@ -26,10 +26,10 @@
img.style.left = "0"; img.style.left = "0";
img.style.pointerEvents = "all"; img.style.pointerEvents = "all";
img.draggable = false; img.draggable = false;
img.addEventListener("mouseenter", ()=> { img.addEventListener("mouseenter", () => {
img.style.left = `${-2 * screenshotWidth}px`; img.style.left = `${-2 * screenshotWidth}px`;
}); });
img.addEventListener("mouseleave", ()=> { img.addEventListener("mouseleave", () => {
img.style.left = "0"; img.style.left = "0";
}); });
var container = document.createElement("div"); var container = document.createElement("div");
@ -52,25 +52,25 @@
container.appendChild(img); container.appendChild(img);
document.body.appendChild(outerContainer); document.body.appendChild(outerContainer);
cleanUp = ()=> { cleanUp = () => {
originalImg.style.opacity = ""; originalImg.style.opacity = "";
container.style.transformOrigin = "center center"; container.style.transformOrigin = "center center";
container.style.transition = "opacity 0.2s ease, transform 0.2s ease"; container.style.transition = "opacity 0.2s ease, transform 0.2s ease";
container.style.opacity = 0; container.style.opacity = 0;
container.style.transform = "scale(0.9)"; container.style.transform = "scale(0.9)";
setTimeout(()=> { setTimeout(() => {
outerContainer.remove(); outerContainer.remove();
}, 500); }, 500);
cleanUp = null; cleanUp = null;
}; };
} }
addEventListener("keydown", e=> { addEventListener("keydown", e => {
if (e.key === "d") { if (e.key === "d") {
if (cleanUp) { if (cleanUp) {
cleanUp(); cleanUp();
} else { } else {
var originalImg = document.elementFromPoint(innerWidth/2, innerHeight/2); var originalImg = document.elementFromPoint(innerWidth / 2, innerHeight / 2);
if (!originalImg || !originalImg.matches("img")) { if (!originalImg || !originalImg.matches("img")) {
console.warn("Didn't find an image in the middle of the page. Found", originalImg); console.warn("Didn't find an image in the middle of the page. Found", originalImg);
return; return;
@ -84,7 +84,7 @@
// mousedown is TAKEN - with stopPropagation, presumably // mousedown is TAKEN - with stopPropagation, presumably
// (useCapture doesn't help) // (useCapture doesn't help)
addEventListener("pointerdown", (e)=> { addEventListener("pointerdown", (e) => {
if (cleanUp) { cleanUp(); } if (cleanUp) { cleanUp(); }
}); });

View File

@ -18,7 +18,7 @@ context('tool tests', () => {
}); });
beforeEach(() => { beforeEach(() => {
if (before_first_real_test) return; if (before_first_real_test) return;
cy.window().then({timeout: 60000}, async (win)=> { cy.window().then({ timeout: 60000 }, async (win) => {
win.selected_colors.foreground = "#000"; win.selected_colors.foreground = "#000";
win.selected_colors.background = "#fff"; win.selected_colors.background = "#fff";
win.brush_shape = win.default_brush_shape; win.brush_shape = win.default_brush_shape;
@ -31,11 +31,11 @@ context('tool tests', () => {
}); });
}); });
const simulateGesture = (win, {start, end, shift, shiftToggleChance=0.01, secondary, secondaryToggleChance, target}) => { const simulateGesture = (win, { start, end, shift, shiftToggleChance = 0.01, secondary, secondaryToggleChance, target }) => {
target = target || win.$(".main-canvas")[0]; target = target || win.$(".main-canvas")[0];
let startWithinRect = target.getBoundingClientRect(); let startWithinRect = target.getBoundingClientRect();
let canvasAreaRect = win.$(".canvas-area")[0].getBoundingClientRect(); let canvasAreaRect = win.$(".canvas-area")[0].getBoundingClientRect();
let startMinX = Math.max(startWithinRect.left, canvasAreaRect.left); let startMinX = Math.max(startWithinRect.left, canvasAreaRect.left);
let startMaxX = Math.min(startWithinRect.right, canvasAreaRect.right); let startMaxX = Math.min(startWithinRect.right, canvasAreaRect.right);
let startMinY = Math.max(startWithinRect.top, canvasAreaRect.top); let startMinY = Math.max(startWithinRect.top, canvasAreaRect.top);
@ -44,7 +44,7 @@ context('tool tests', () => {
let startPointY = startMinY + start.y * (startMaxY - startMinY); let startPointY = startMinY + start.y * (startMaxY - startMinY);
let endPointX = startMinX + end.x * (startMaxX - startMinX); let endPointX = startMinX + end.x * (startMaxX - startMinX);
let endPointY = startMinY + end.y * (startMaxY - startMinY); let endPointY = startMinY + end.y * (startMaxY - startMinY);
const $cursor = win.$(`<img src="images/cursors/default.png" class="user-cursor"/>`); const $cursor = win.$(`<img src="images/cursors/default.png" class="user-cursor"/>`);
$cursor.css({ $cursor.css({
position: "absolute", position: "absolute",
@ -57,7 +57,7 @@ context('tool tests', () => {
}); });
$cursor.appendTo(".jspaint"); $cursor.appendTo(".jspaint");
let triggerMouseEvent = (type, point) => { let triggerMouseEvent = (type, point) => {
const clientX = point.x; const clientX = point.x;
const clientY = point.y; const clientY = point.y;
// const el_over = win.document.elementFromPoint(clientX, clientY); // const el_over = win.document.elementFromPoint(clientX, clientY);
@ -72,7 +72,7 @@ context('tool tests', () => {
if (do_nothing) { if (do_nothing) {
return; return;
} }
let event = new win.$.Event(type, { let event = new win.$.Event(type, {
view: window, view: window,
bubbles: true, bubbles: true,
@ -89,7 +89,7 @@ context('tool tests', () => {
}); });
win.$(target).trigger(event); win.$(target).trigger(event);
}; };
let t = 0; let t = 0;
const stepsInGesture = 3; const stepsInGesture = 3;
let pointForTime = (t) => { let pointForTime = (t) => {
@ -98,8 +98,8 @@ context('tool tests', () => {
y: startPointY + (endPointY - startPointY) * Math.pow(t, 0.3), y: startPointY + (endPointY - startPointY) * Math.pow(t, 0.3),
}; };
}; };
return new Promise((resolve)=> { return new Promise((resolve) => {
triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor
triggerMouseEvent("pointerdown", pointForTime(t)); triggerMouseEvent("pointerdown", pointForTime(t));
let move = () => { let move = () => {
@ -112,9 +112,9 @@ context('tool tests', () => {
// } // }
if (t > 1) { if (t > 1) {
triggerMouseEvent("pointerup", pointForTime(t)); triggerMouseEvent("pointerup", pointForTime(t));
$cursor.remove(); $cursor.remove();
resolve(); resolve();
} else { } else {
triggerMouseEvent("pointermove", pointForTime(t)); triggerMouseEvent("pointermove", pointForTime(t));
@ -160,30 +160,30 @@ context('tool tests', () => {
it(`eraser tool`, () => { it(`eraser tool`, () => {
cy.get(`.tool[title='Eraser/Color Eraser']`).click(); cy.get(`.tool[title='Eraser/Color Eraser']`).click();
// gesture([{x: 50, y: 50}, {x: 100, y: 100}]); // gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
cy.window().then({timeout: 60000}, async (win)=> { cy.window().then({ timeout: 60000 }, async (win) => {
for (let row=0; row<4; row++) { for (let row = 0; row < 4; row++) {
const secondary = !!(row % 2); const secondary = !!(row % 2);
const increaseSize = row >= 2; const increaseSize = row >= 2;
let $options = win.$(`.chooser > *`); let $options = win.$(`.chooser > *`);
for (let o=0; o<$options.length; o++) { for (let o = 0; o < $options.length; o++) {
$options[o].click(); $options[o].click();
if (increaseSize) { if (increaseSize) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
win.$('body').trigger(new win.$.Event("keydown", {key: "NumpadPlus", keyCode: 107, which: 107})); win.$('body').trigger(new win.$.Event("keydown", { key: "NumpadPlus", keyCode: 107, which: 107 }));
} }
} }
win.selected_colors.background = "#f0f"; win.selected_colors.background = "#f0f";
const start = {x: 0.05 + o*0.05, y: 0.1 + 0.1*row}; const start = { x: 0.05 + o * 0.05, y: 0.1 + 0.1 * row };
const end = {x: start.x + 0.04, y: start.y + 0.04}; const end = { x: start.x + 0.04, y: start.y + 0.04 };
await simulateGesture(win, {shift: false, secondary: false, start, end}); await simulateGesture(win, { shift: false, secondary: false, start, end });
if (secondary) { if (secondary) {
// eslint-disable-next-line require-atomic-updates // eslint-disable-next-line require-atomic-updates
win.selected_colors.background = "#ff0"; win.selected_colors.background = "#ff0";
// eslint-disable-next-line require-atomic-updates // eslint-disable-next-line require-atomic-updates
win.selected_colors.foreground = "#f0f"; win.selected_colors.foreground = "#f0f";
const start = {x: 0.04 + o*0.05, y: 0.11 + 0.1*row}; const start = { x: 0.04 + o * 0.05, y: 0.11 + 0.1 * row };
const end = {x: start.x + 0.03, y: start.y + 0.02}; const end = { x: start.x + 0.03, y: start.y + 0.02 };
await simulateGesture(win, {shift: false, secondary: true, start, end}); await simulateGesture(win, { shift: false, secondary: true, start, end });
} }
} }
} }
@ -191,13 +191,13 @@ context('tool tests', () => {
cy.get(".main-canvas").matchImageSnapshot(); cy.get(".main-canvas").matchImageSnapshot();
}); });
["Brush", "Pencil", "Rectangle", "Rounded Rectangle", "Ellipse", "Line"].forEach((toolName)=> { ["Brush", "Pencil", "Rectangle", "Rounded Rectangle", "Ellipse", "Line"].forEach((toolName) => {
it(`${toolName.toLowerCase()} tool`, () => { it(`${toolName.toLowerCase()} tool`, () => {
cy.get(`.tool[title='${toolName}']`).click(); cy.get(`.tool[title='${toolName}']`).click();
// gesture([{x: 50, y: 50}, {x: 100, y: 100}]); // gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
cy.get(".swatch:nth-child(22)").rightclick(); cy.get(".swatch:nth-child(22)").rightclick();
cy.window().then({timeout: 60000}, async (win)=> { cy.window().then({ timeout: 60000 }, async (win) => {
for (let row=0; row<4; row++) { for (let row = 0; row < 4; row++) {
const secondary = !!(row % 2); const secondary = !!(row % 2);
const increaseSize = row >= 2; const increaseSize = row >= 2;
let $options = win.$(`.chooser > *`); let $options = win.$(`.chooser > *`);
@ -205,16 +205,16 @@ context('tool tests', () => {
if ($options.length === 0) { if ($options.length === 0) {
$options = win.$("<dummy>"); $options = win.$("<dummy>");
} }
for (let o=0; o<$options.length; o++) { for (let o = 0; o < $options.length; o++) {
$options[o].click(); $options[o].click();
if (increaseSize && (o === 0 || toolName==="Brush" || toolName==="Line")) { if (increaseSize && (o === 0 || toolName === "Brush" || toolName === "Line")) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
win.$('body').trigger(new win.$.Event("keydown", {key: "NumpadPlus", keyCode: 107, which: 107})); win.$('body').trigger(new win.$.Event("keydown", { key: "NumpadPlus", keyCode: 107, which: 107 }));
} }
} }
const start = {x: 0.05 + o*0.05, y: 0.1 + 0.1*row}; const start = { x: 0.05 + o * 0.05, y: 0.1 + 0.1 * row };
const end = {x: start.x + 0.04, y: start.y + 0.04}; const end = { x: start.x + 0.04, y: start.y + 0.04 };
await simulateGesture(win, {shift: false, secondary: !!secondary, start, end}); await simulateGesture(win, { shift: false, secondary: !!secondary, start, end });
} }
} }
}); });

View File

@ -59,7 +59,7 @@ context('visual tests', () => {
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions); cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
}); });
beforeEach(()=> { beforeEach(() => {
if (Cypress.$('.window:visible')[0]) { if (Cypress.$('.window:visible')[0]) {
cy.get('.window:visible .window-close-button').click(); cy.get('.window:visible .window-close-button').click();
cy.get('.window').should('not.be.visible'); cy.get('.window').should('not.be.visible');
@ -90,7 +90,7 @@ context('visual tests', () => {
// @TODO: make menus more testable, with IDs // @TODO: make menus more testable, with IDs
cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click(); cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click();
cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(1)').click(); cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(1)').click();
cy.get('.window:visible .folder', {timeout: 10000}); // wait for sidebar contents to load cy.get('.window:visible .folder', { timeout: 10000 }); // wait for sidebar contents to load
// @TODO: wait for iframe to load // @TODO: wait for iframe to load
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions, { blackout: ["iframe"] })); cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions, { blackout: ["iframe"] }));
}); });
@ -133,7 +133,7 @@ context('visual tests', () => {
cy.matchImageSnapshot(withTextCompareOptions); cy.matchImageSnapshot(withTextCompareOptions);
}); });
const test_edit_colors_dialog = (expand=true) => { const test_edit_colors_dialog = (expand = true) => {
cy.contains(".menu-button", "Colors").click(); cy.contains(".menu-button", "Colors").click();
cy.contains(".menu-item", "Edit Colors").click(); cy.contains(".menu-item", "Edit Colors").click();
cy.wait(100); cy.wait(100);
@ -169,7 +169,7 @@ context('visual tests', () => {
cy.matchImageSnapshot(withTextCompareOptions); cy.matchImageSnapshot(withTextCompareOptions);
}); });
it('classic theme edit colors dialog', ()=> { it('classic theme edit colors dialog', () => {
test_edit_colors_dialog(false); test_edit_colors_dialog(false);
}); });

1309
index.html

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
/** Used by the Colors Box and by the Edit Colors dialog */ /** Used by the Colors Box and by the Edit Colors dialog */
function $Swatch(color){ function $Swatch(color) {
const $swatch = $(E("div")).addClass("swatch"); const $swatch = $(E("div")).addClass("swatch");
const swatch_canvas = make_canvas(); const swatch_canvas = make_canvas();
$(swatch_canvas).css({pointerEvents: "none"}).appendTo($swatch); $(swatch_canvas).css({ pointerEvents: "none" }).appendTo($swatch);
// @TODO: clean up event listener // @TODO: clean up event listener
$G.on("theme-load", ()=> { update_$swatch($swatch); }); $G.on("theme-load", () => { update_$swatch($swatch); });
$swatch.data("swatch", color); $swatch.data("swatch", color);
update_$swatch($swatch, color); update_$swatch($swatch, color);
return $swatch; return $swatch;
} }
@ -37,36 +37,36 @@ function update_$swatch($swatch, new_color) {
}); });
} }
function $ColorBox(vertical){ function $ColorBox(vertical) {
const $cb = $(E("div")).addClass("color-box"); const $cb = $(E("div")).addClass("color-box");
const $current_colors = $Swatch(selected_colors.ternary).addClass("current-colors"); const $current_colors = $Swatch(selected_colors.ternary).addClass("current-colors");
const $palette = $(E("div")).addClass("palette"); const $palette = $(E("div")).addClass("palette");
$cb.append($current_colors, $palette); $cb.append($current_colors, $palette);
const $foreground_color = $Swatch(selected_colors.foreground).addClass("color-selection foreground-color"); const $foreground_color = $Swatch(selected_colors.foreground).addClass("color-selection foreground-color");
const $background_color = $Swatch(selected_colors.background).addClass("color-selection background-color"); const $background_color = $Swatch(selected_colors.background).addClass("color-selection background-color");
$current_colors.append($background_color, $foreground_color); $current_colors.append($background_color, $foreground_color);
$G.on("option-changed", () => { $G.on("option-changed", () => {
update_$swatch($foreground_color, selected_colors.foreground); update_$swatch($foreground_color, selected_colors.foreground);
update_$swatch($background_color, selected_colors.background); update_$swatch($background_color, selected_colors.background);
update_$swatch($current_colors, selected_colors.ternary); update_$swatch($current_colors, selected_colors.ternary);
}); });
$current_colors.on("pointerdown", () => { $current_colors.on("pointerdown", () => {
const new_bg = selected_colors.foreground; const new_bg = selected_colors.foreground;
selected_colors.foreground = selected_colors.background; selected_colors.foreground = selected_colors.background;
selected_colors.background = new_bg; selected_colors.background = new_bg;
$G.triggerHandler("option-changed"); $G.triggerHandler("option-changed");
}); });
const make_color_button = (color) => { const make_color_button = (color) => {
const $b = $Swatch(color).addClass("color-button"); const $b = $Swatch(color).addClass("color-button");
$b.appendTo($palette); $b.appendTo($palette);
const double_click_period_ms = 400; const double_click_period_ms = 400;
let within_double_click_period = false; let within_double_click_period = false;
let double_click_button = null; let double_click_button = null;
@ -77,10 +77,10 @@ function $ColorBox(vertical){
// @TODO: allow metaKey for ternary color, and selection cropping, on macOS? // @TODO: allow metaKey for ternary color, and selection cropping, on macOS?
ctrl = e.ctrlKey; ctrl = e.ctrlKey;
button = e.button; button = e.button;
if(button === 0){ if (button === 0) {
$c.data("$last_fg_color_button", $b); $c.data("$last_fg_color_button", $b);
} }
const color_selection_slot = ctrl ? "ternary" : button === 0 ? "foreground" : button === 2 ? "background" : null; const color_selection_slot = ctrl ? "ternary" : button === 0 ? "foreground" : button === 2 ? "background" : null;
if (color_selection_slot) { if (color_selection_slot) {
if (within_double_click_period && button === double_click_button) { if (within_double_click_period && button === double_click_button) {
@ -89,7 +89,7 @@ function $ColorBox(vertical){
selected_colors[color_selection_slot] = $b.data("swatch"); selected_colors[color_selection_slot] = $b.data("swatch");
$G.trigger("option-changed"); $G.trigger("option-changed");
} }
clearTimeout(double_click_tid); clearTimeout(double_click_tid);
double_click_tid = setTimeout(() => { double_click_tid = setTimeout(() => {
within_double_click_period = false; within_double_click_period = false;
@ -113,13 +113,13 @@ function $ColorBox(vertical){
$some_button.outerHeight() + $some_button.outerHeight() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-top")) + parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-top")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-bottom")); parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-bottom"));
$palette.height(Math.ceil(palette.length/2) * height_per_button); $palette.height(Math.ceil(palette.length / 2) * height_per_button);
} else { } else {
const width_per_button = const width_per_button =
$some_button.outerWidth() + $some_button.outerWidth() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-left")) + parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-right")); parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-right"));
$palette.width(Math.ceil(palette.length/2) * width_per_button); $palette.width(Math.ceil(palette.length / 2) * width_per_button);
} }
// the "last foreground color button" starts out as the first in the palette // the "last foreground color button" starts out as the first in the palette
@ -129,15 +129,15 @@ function $ColorBox(vertical){
if (vertical) { if (vertical) {
$c = $Component(localize("Colors"), "colors-component", "tall", $cb); $c = $Component(localize("Colors"), "colors-component", "tall", $cb);
$c.appendTo(get_direction() === "rtl" ? $left : $right); // opposite ToolBox by default $c.appendTo(get_direction() === "rtl" ? $left : $right); // opposite ToolBox by default
}else{ } else {
$c = $Component(localize("Colors"), "colors-component", "wide", $cb); $c = $Component(localize("Colors"), "colors-component", "wide", $cb);
$c.appendTo($bottom); $c.appendTo($bottom);
} }
build_palette(); build_palette();
$(window).on("theme-change", build_palette); $(window).on("theme-change", build_palette);
$c.rebuild_palette = build_palette; $c.rebuild_palette = build_palette;
return $c; return $c;
} }

View File

@ -5,8 +5,8 @@
function get_segments(component_area_el, pos_axis, exclude_component_el) { function get_segments(component_area_el, pos_axis, exclude_component_el) {
const $other_components = $(component_area_el).find(".component").not(exclude_component_el); const $other_components = $(component_area_el).find(".component").not(exclude_component_el);
return $other_components.toArray().map((component_el)=> { return $other_components.toArray().map((component_el) => {
const segment = {element: component_el}; const segment = { element: component_el };
if (pos_axis === "top") { if (pos_axis === "top") {
segment.pos = component_el.offsetTop; segment.pos = component_el.offsetTop;
segment.length = component_el.clientHeight; segment.length = component_el.clientHeight;
@ -22,7 +22,7 @@ function get_segments(component_area_el, pos_axis, exclude_component_el) {
} }
function adjust_segments(segments, total_available_length) { function adjust_segments(segments, total_available_length) {
segments.sort((a, b)=> a.pos - b.pos); segments.sort((a, b) => a.pos - b.pos);
// Clamp // Clamp
for (const segment of segments) { for (const segment of segments) {
@ -72,14 +72,14 @@ function apply_segments(component_area_el, pos_axis, segments) {
} }
} }
function $Component(title, className, orientation, $el){ function $Component(title, className, orientation, $el) {
// A draggable widget that can be undocked into a window // A draggable widget that can be undocked into a window
const $c = $(E("div")).addClass("component"); const $c = $(E("div")).addClass("component");
$c.addClass(className); $c.addClass(className);
$c.addClass(orientation); $c.addClass(orientation);
$c.append($el); $c.append($el);
$c.css("touch-action", "none"); $c.css("touch-action", "none");
const $w = new $ToolWindow($c); const $w = new $ToolWindow($c);
$w.title(title); $w.title(title);
$w.hide(); $w.hide();
@ -87,17 +87,17 @@ function $Component(title, className, orientation, $el){
tall: "vertical", tall: "vertical",
wide: "horizontal", wide: "horizontal",
}[orientation]); }[orientation]);
// Nudge the Colors component over a tiny bit // Nudge the Colors component over a tiny bit
if(className === "colors-component" && orientation === "wide"){ if (className === "colors-component" && orientation === "wide") {
$c.css("position", "relative"); $c.css("position", "relative");
$c.css(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px"); $c.css(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px");
} }
let iid; let iid;
if($("body").hasClass("eye-gaze-mode")){ if ($("body").hasClass("eye-gaze-mode")) {
// @TODO: don't use an interval for this! // @TODO: don't use an interval for this!
iid = setInterval(()=> { iid = setInterval(() => {
const scale = 3; const scale = 3;
$c.css({ $c.css({
transform: `scale(${scale})`, transform: `scale(${scale})`,
@ -107,7 +107,7 @@ function $Component(title, className, orientation, $el){
}); });
}, 200); }, 200);
} }
let ox, oy; let ox, oy;
let ox2, oy2; let ox2, oy2;
let w, h; let w, h;
@ -117,15 +117,15 @@ function $Component(title, className, orientation, $el){
let $last_docked_to; let $last_docked_to;
let $dock_to; let $dock_to;
let $ghost; let $ghost;
if(orientation === "tall"){ if (orientation === "tall") {
pos_axis = "top"; pos_axis = "top";
}else if(get_direction() === "rtl"){ } else if (get_direction() === "rtl") {
pos_axis = "right"; pos_axis = "right";
}else{ } else {
pos_axis = "left"; pos_axis = "left";
} }
const dock_to = $dock_to => { const dock_to = $dock_to => {
$w.hide(); $w.hide();
@ -145,7 +145,7 @@ function $Component(title, className, orientation, $el){
// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val)); // console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
adjust_segments(segments, total_available_length); adjust_segments(segments, total_available_length);
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val)); // console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
apply_segments($dock_to[0], pos_axis, segments); apply_segments($dock_to[0], pos_axis, segments);
// Save where it's now docked to // Save where it's now docked to
@ -156,7 +156,7 @@ function $Component(title, className, orientation, $el){
const component_area_el = $c.closest(".component-area")[0]; const component_area_el = $c.closest(".component-area")[0];
// must get layout state *before* changing it // must get layout state *before* changing it
const segments = get_segments(component_area_el, pos_axis, $c[0]); const segments = get_segments(component_area_el, pos_axis, $c[0]);
$c.css("position", "relative"); $c.css("position", "relative");
$c.css(`margin-${pos_axis}`, ""); $c.css(`margin-${pos_axis}`, "");
@ -175,15 +175,15 @@ function $Component(title, className, orientation, $el){
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val)); // console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
apply_segments(component_area_el, pos_axis, segments); apply_segments(component_area_el, pos_axis, segments);
}; };
$w.on("window-drag-start", (e)=> { $w.on("window-drag-start", (e) => {
e.preventDefault(); e.preventDefault();
}); });
const imagine_window_dimensions = ()=> { const imagine_window_dimensions = () => {
const prev_window_shown = $w.is(":visible"); const prev_window_shown = $w.is(":visible");
$w.show(); $w.show();
let $spacer; let $spacer;
let {offsetLeft, offsetTop} = $c[0]; let { offsetLeft, offsetTop } = $c[0];
if ($c.closest(".tool-window").length == 0) { if ($c.closest(".tool-window").length == 0) {
const styles = getComputedStyle($c[0]); const styles = getComputedStyle($c[0]);
$spacer = $(E("div")).addClass("component").css({ $spacer = $(E("div")).addClass("component").css({
@ -194,7 +194,7 @@ function $Component(title, className, orientation, $el){
// let padding be influenced by CSS // let padding be influenced by CSS
}); });
$w.append($spacer); $w.append($spacer);
({offsetLeft, offsetTop} = $spacer[0]); ({ offsetLeft, offsetTop } = $spacer[0]);
} }
const rect = $w[0].getBoundingClientRect(); const rect = $w[0].getBoundingClientRect();
if ($spacer) { if ($spacer) {
@ -206,11 +206,11 @@ function $Component(title, className, orientation, $el){
const w_styles = getComputedStyle($w[0]); const w_styles = getComputedStyle($w[0]);
offsetLeft += parseFloat(w_styles.borderLeftWidth); offsetLeft += parseFloat(w_styles.borderLeftWidth);
offsetTop += parseFloat(w_styles.borderTopWidth); offsetTop += parseFloat(w_styles.borderTopWidth);
return {rect, offsetLeft, offsetTop}; return { rect, offsetLeft, offsetTop };
}; };
const imagine_docked_dimensions = ($dock_to=(pos_axis === "top" ? $left : $bottom))=> { const imagine_docked_dimensions = ($dock_to = (pos_axis === "top" ? $left : $bottom)) => {
if ($c.closest(".tool-window").length == 0) { if ($c.closest(".tool-window").length == 0) {
return {rect: $c[0].getBoundingClientRect()}; return { rect: $c[0].getBoundingClientRect() };
} }
const styles = getComputedStyle($c[0]); const styles = getComputedStyle($c[0]);
const $spacer = $(E("div")).addClass("component").css({ const $spacer = $(E("div")).addClass("component").css({
@ -223,18 +223,18 @@ function $Component(title, className, orientation, $el){
if ($spacer) { if ($spacer) {
$spacer.remove(); $spacer.remove();
} }
return {rect}; return { rect };
}; };
const render_ghost = (e)=> { const render_ghost = (e) => {
const {rect} = $dock_to ? imagine_docked_dimensions($dock_to) : imagine_window_dimensions() const { rect } = $dock_to ? imagine_docked_dimensions($dock_to) : imagine_window_dimensions()
// Make sure these dimensions are odd numbers // Make sure these dimensions are odd numbers
// so the alternating pattern of the border is unbroken // so the alternating pattern of the border is unbroken
w = (~~(rect.width/2))*2 + 1; w = (~~(rect.width / 2)) * 2 + 1;
h = (~~(rect.height/2))*2 + 1; h = (~~(rect.height / 2)) * 2 + 1;
if(!$ghost){ if (!$ghost) {
$ghost = $(E("div")).addClass("component-ghost dock"); $ghost = $(E("div")).addClass("component-ghost dock");
$ghost.appendTo("body"); $ghost.appendTo("body");
} }
@ -248,88 +248,88 @@ function $Component(title, className, orientation, $el){
top: e.clientY + ($dock_to ? oy : oy2) + inset, top: e.clientY + ($dock_to ? oy : oy2) + inset,
}); });
if($dock_to){ if ($dock_to) {
$ghost.addClass("dock"); $ghost.addClass("dock");
}else{ } else {
$ghost.removeClass("dock"); $ghost.removeClass("dock");
} }
}; };
$c.add($w.$titlebar).on("pointerdown", e => { $c.add($w.$titlebar).on("pointerdown", e => {
// Only start a drag via a left click directly on the component element or titlebar // Only start a drag via a left click directly on the component element or titlebar
if(e.button !== 0){ return; } if (e.button !== 0) { return; }
const validTarget = const validTarget =
$c.is(e.target) || $c.is(e.target) ||
( (
$(e.target).closest($w.$titlebar).length > 0 && $(e.target).closest($w.$titlebar).length > 0 &&
$(e.target).closest("button").length === 0 $(e.target).closest("button").length === 0
); );
if(!validTarget){ return; } if (!validTarget) { return; }
// Don't allow dragging in eye gaze mode // Don't allow dragging in eye gaze mode
if($("body").hasClass("eye-gaze-mode")){ return; } if ($("body").hasClass("eye-gaze-mode")) { return; }
const docked = imagine_docked_dimensions(); const docked = imagine_docked_dimensions();
const rect = $c[0].getBoundingClientRect(); const rect = $c[0].getBoundingClientRect();
ox = rect.left - e.clientX; ox = rect.left - e.clientX;
oy = rect.top - e.clientY; oy = rect.top - e.clientY;
ox = -Math.min(Math.max(-ox, 0), docked.rect.width); ox = -Math.min(Math.max(-ox, 0), docked.rect.width);
oy = -Math.min(Math.max(-oy, 0), docked.rect.height); oy = -Math.min(Math.max(-oy, 0), docked.rect.height);
const {offsetLeft, offsetTop} = imagine_window_dimensions(); const { offsetLeft, offsetTop } = imagine_window_dimensions();
ox2 = rect.left - offsetLeft - e.clientX; ox2 = rect.left - offsetLeft - e.clientX;
oy2 = rect.top - offsetTop - e.clientY; oy2 = rect.top - offsetTop - e.clientY;
$("body").addClass("dragging"); $("body").addClass("dragging");
$("body").css({cursor: "default"}).addClass("cursor-bully"); $("body").css({ cursor: "default" }).addClass("cursor-bully");
$G.on("pointermove", drag_update_position); $G.on("pointermove", drag_update_position);
$G.one("pointerup", e => { $G.one("pointerup", e => {
$G.off("pointermove", drag_update_position); $G.off("pointermove", drag_update_position);
drag_onpointerup(e); drag_onpointerup(e);
$("body").removeClass("dragging"); $("body").removeClass("dragging");
$("body").css({cursor: ""}).removeClass("cursor-bully"); $("body").css({ cursor: "" }).removeClass("cursor-bully");
$canvas.trigger("pointerleave"); // prevent magnifier preview showing until you move the mouse $canvas.trigger("pointerleave"); // prevent magnifier preview showing until you move the mouse
}); });
render_ghost(e); render_ghost(e);
drag_update_position(e); drag_update_position(e);
// Prevent text selection anywhere within the component // Prevent text selection anywhere within the component
e.preventDefault(); e.preventDefault();
}); });
const drag_update_position = e => { const drag_update_position = e => {
$ghost.css({ $ghost.css({
left: e.clientX + ox, left: e.clientX + ox,
top: e.clientY + oy, top: e.clientY + oy,
}); });
$dock_to = null; $dock_to = null;
const {width, height} = imagine_docked_dimensions().rect; const { width, height } = imagine_docked_dimensions().rect;
const dock_ghost_left = e.clientX + ox; const dock_ghost_left = e.clientX + ox;
const dock_ghost_top = e.clientY + oy; const dock_ghost_top = e.clientY + oy;
const dock_ghost_right = dock_ghost_left + width; const dock_ghost_right = dock_ghost_left + width;
const dock_ghost_bottom = dock_ghost_top + height; const dock_ghost_bottom = dock_ghost_top + height;
const q = 5; const q = 5;
if(orientation === "tall"){ if (orientation === "tall") {
pos_axis = "top"; pos_axis = "top";
if(dock_ghost_left-q < $left[0].getBoundingClientRect().right){ if (dock_ghost_left - q < $left[0].getBoundingClientRect().right) {
$dock_to = $left; $dock_to = $left;
} }
if(dock_ghost_right+q > $right[0].getBoundingClientRect().left){ if (dock_ghost_right + q > $right[0].getBoundingClientRect().left) {
$dock_to = $right; $dock_to = $right;
} }
}else{ } else {
pos_axis = get_direction() === "rtl" ? "right" : "left"; pos_axis = get_direction() === "rtl" ? "right" : "left";
if(dock_ghost_top-q < $top[0].getBoundingClientRect().bottom){ if (dock_ghost_top - q < $top[0].getBoundingClientRect().bottom) {
$dock_to = $top; $dock_to = $top;
} }
if(dock_ghost_bottom+q > $bottom[0].getBoundingClientRect().top){ if (dock_ghost_bottom + q > $bottom[0].getBoundingClientRect().top) {
$dock_to = $bottom; $dock_to = $bottom;
} }
} }
if($dock_to){ if ($dock_to) {
const dock_to_rect = $dock_to[0].getBoundingClientRect(); const dock_to_rect = $dock_to[0].getBoundingClientRect();
pos = ( pos = (
pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left
@ -343,42 +343,42 @@ function $Component(title, className, orientation, $el){
e.preventDefault(); e.preventDefault();
}; };
const drag_onpointerup = e => { const drag_onpointerup = e => {
$w.hide(); $w.hide();
// If the component is docked to a component area (a side) // If the component is docked to a component area (a side)
if($c.parent().is(".component-area")){ if ($c.parent().is(".component-area")) {
// Save where it's docked so we can dock back later // Save where it's docked so we can dock back later
$last_docked_to = $c.parent(); $last_docked_to = $c.parent();
if($dock_to){ if ($dock_to) {
last_docked_to_pos = pos; last_docked_to_pos = pos;
} }
} }
if($dock_to){ if ($dock_to) {
// Dock component to $dock_to // Dock component to $dock_to
dock_to($dock_to); dock_to($dock_to);
} else { } else {
undock_to(e.clientX + ox2, e.clientY + oy2); undock_to(e.clientX + ox2, e.clientY + oy2);
} }
$ghost && $ghost.remove(); $ghost && $ghost.remove();
$ghost = null; $ghost = null;
$G.trigger("resize"); $G.trigger("resize");
}; };
$c.dock = ($dock_to) => { $c.dock = ($dock_to) => {
pos = last_docked_to_pos ?? 0; pos = last_docked_to_pos ?? 0;
dock_to($dock_to ?? $last_docked_to); dock_to($dock_to ?? $last_docked_to);
}; };
$c.undock_to = undock_to; $c.undock_to = undock_to;
$c.show = () => { $c.show = () => {
$($c[0]).show(); // avoid recursion $($c[0]).show(); // avoid recursion
if($.contains($w[0], $c[0])){ if ($.contains($w[0], $c[0])) {
$w.show(); $w.show();
} }
return $c; return $c;
@ -388,23 +388,23 @@ function $Component(title, className, orientation, $el){
return $c; return $c;
}; };
$c.toggle = () => { $c.toggle = () => {
if($c.is(":visible")){ if ($c.is(":visible")) {
$c.hide(); $c.hide();
}else{ } else {
$c.show(); $c.show();
} }
return $c; return $c;
}; };
$c.destroy = ()=> { $c.destroy = () => {
$w.close(); $w.close();
$c.remove(); $c.remove();
clearInterval(iid); clearInterval(iid);
}; };
$w.on("close", e => { $w.on("close", e => {
e.preventDefault(); e.preventDefault();
$w.hide(); $w.hide();
}); });
return $c; return $c;
} }

View File

@ -1,7 +1,7 @@
function $FontBox(){ function $FontBox() {
const $fb = $(E("div")).addClass("font-box"); const $fb = $(E("div")).addClass("font-box");
const $family = $(E("select")).addClass("inset-deep").attr({ const $family = $(E("select")).addClass("inset-deep").attr({
"aria-label": "Font Family", "aria-label": "Font Family",
"aria-description": localize("Selects the font used by the text."), "aria-description": localize("Selects the font used by the text."),
@ -26,13 +26,13 @@ function $FontBox(){
$button_group.append($bold, $italic, $underline, $vertical); $button_group.append($bold, $italic, $underline, $vertical);
$fb.append($family, $size, $button_group); $fb.append($family, $size, $button_group);
const update_font = () => { const update_font = () => {
text_tool_font.size = Number($size.val()); text_tool_font.size = Number($size.val());
text_tool_font.family = $family.val(); text_tool_font.family = $family.val();
$G.trigger("option-changed"); $G.trigger("option-changed");
}; };
FontDetective.each(font => { FontDetective.each(font => {
const $option = $(E("option")); const $option = $(E("option"));
$option.val(font).text(font.name); $option.val(font).text(font.name);
@ -41,21 +41,21 @@ function $FontBox(){
update_font(); update_font();
} }
}); });
if (text_tool_font.family) { if (text_tool_font.family) {
$family.val(text_tool_font.family); $family.val(text_tool_font.family);
} }
$family.on("change", update_font); $family.on("change", update_font);
$size.on("change", update_font); $size.on("change", update_font);
const $w = $ToolWindow(); const $w = $ToolWindow();
$w.title(localize("Fonts")); $w.title(localize("Fonts"));
$w.$content.append($fb); $w.$content.append($fb);
$w.center(); $w.center();
return $w; return $w;
function $Toggle(xi, thing, label, description) { function $Toggle(xi, thing, label, description) {
const $button = $(E("button")).addClass("toggle").attr({ const $button = $(E("button")).addClass("toggle").attr({
"aria-pressed": false, "aria-pressed": false,
@ -85,7 +85,7 @@ function $FontBox(){
$button.attr("aria-pressed", $button.hasClass("selected")); $button.attr("aria-pressed", $button.hasClass("selected"));
update_font(); update_font();
}); });
if(text_tool_font[thing]){ if (text_tool_font[thing]) {
$button.addClass("selected").attr("aria-pressed", true); $button.addClass("selected").attr("aria-pressed", true);
} }
return $button; return $button;

View File

@ -1,25 +1,25 @@
let theme_dev_blob_url; let theme_dev_blob_url;
function $ToolBox(tools, is_extras){ function $ToolBox(tools, is_extras) {
const $tools = $(E("div")).addClass("tools"); const $tools = $(E("div")).addClass("tools");
const $tool_options = $(E("div")).addClass("tool-options"); const $tool_options = $(E("div")).addClass("tool-options");
let showing_tooltips = false; let showing_tooltips = false;
$tools.on("pointerleave", () => { $tools.on("pointerleave", () => {
showing_tooltips = false; showing_tooltips = false;
$status_text.default(); $status_text.default();
}); });
const $buttons = $($.map(tools, (tool, i) => { const $buttons = $($.map(tools, (tool, i) => {
const $b = $(E("div")).addClass("tool"); const $b = $(E("div")).addClass("tool");
$b.appendTo($tools); $b.appendTo($tools);
tool.$button = $b; tool.$button = $b;
$b.attr("title", tool.name); $b.attr("title", tool.name);
const $icon = $(E("span")).addClass("tool-icon"); const $icon = $(E("span")).addClass("tool-icon");
$icon.appendTo($b); $icon.appendTo($b);
const update_css = ()=> { const update_css = () => {
const use_svg = !theme_dev_blob_url && ( const use_svg = !theme_dev_blob_url && (
( (
(window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0) (window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0)
@ -40,37 +40,37 @@ function $ToolBox(tools, is_extras){
}; };
update_css(); update_css();
$G.on("theme-load resize", update_css); $G.on("theme-load resize", update_css);
$b.on("click", e => { $b.on("click", e => {
if (e.shiftKey || e.ctrlKey) { if (e.shiftKey || e.ctrlKey) {
select_tool(tool, true); select_tool(tool, true);
return; return;
} }
if(selected_tool === tool && tool.deselect){ if (selected_tool === tool && tool.deselect) {
select_tools(return_to_tools); select_tools(return_to_tools);
}else{ } else {
select_tool(tool); select_tool(tool);
} }
}); });
$b.on("pointerenter", () => { $b.on("pointerenter", () => {
const show_tooltip = () => { const show_tooltip = () => {
showing_tooltips = true; showing_tooltips = true;
$status_text.text(tool.description); $status_text.text(tool.description);
}; };
if(showing_tooltips){ if (showing_tooltips) {
show_tooltip(); show_tooltip();
}else{ } else {
const tid = setTimeout(show_tooltip, 300); const tid = setTimeout(show_tooltip, 300);
$b.on("pointerleave", () => { $b.on("pointerleave", () => {
clearTimeout(tid); clearTimeout(tid);
}); });
} }
}); });
return $b[0]; return $b[0];
})); }));
const $c = $Component( const $c = $Component(
is_extras ? "Extra Tools" : localize("Tools"), is_extras ? "Extra Tools" : localize("Tools"),
is_extras ? "tools-component extra-tools-component" : "tools-component", is_extras ? "tools-component extra-tools-component" : "tools-component",
@ -80,7 +80,7 @@ function $ToolBox(tools, is_extras){
$c.appendTo(get_direction() === "rtl" ? $right : $left); // opposite ColorBox by default $c.appendTo(get_direction() === "rtl" ? $right : $left); // opposite ColorBox by default
$c.update_selected_tool = () => { $c.update_selected_tool = () => {
$buttons.removeClass("selected"); $buttons.removeClass("selected");
selected_tools.forEach((selected_tool)=> { selected_tools.forEach((selected_tool) => {
selected_tool.$button.addClass("selected"); selected_tool.$button.addClass("selected");
}); });
$tool_options.children().detach(); $tool_options.children().detach();
@ -102,14 +102,14 @@ function $ToolBox(tools, is_extras){
let dev_theme_tool_icons = false; let dev_theme_tool_icons = false;
try { try {
dev_theme_tool_icons = localStorage.dev_theme_tool_icons === "true"; dev_theme_tool_icons = localStorage.dev_theme_tool_icons === "true";
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) { } } catch (e) { }
if (dev_theme_tool_icons) { if (dev_theme_tool_icons) {
let last_update_id = 0; let last_update_id = 0;
$G.on("session-update", ()=> { $G.on("session-update", () => {
last_update_id += 1; last_update_id += 1;
const this_update_id = last_update_id; const this_update_id = last_update_id;
main_canvas.toBlob((blob)=> { main_canvas.toBlob((blob) => {
// avoid a race condition particularly when loading the document initially when the default canvas size is large, giving a larger PNG // avoid a race condition particularly when loading the document initially when the default canvas size is large, giving a larger PNG
if (this_update_id !== last_update_id) { if (this_update_id !== last_update_id) {
return; return;

View File

@ -2,7 +2,7 @@
function make_window_supporting_scale(options) { function make_window_supporting_scale(options) {
const $w = new $Window(options); const $w = new $Window(options);
const scale_for_eye_gaze_mode_and_center = ()=> { const scale_for_eye_gaze_mode_and_center = () => {
if (!$w.is(".edit-colors-window, .storage-manager, .attributes-window, .flip-and-rotate, .stretch-and-skew")) { if (!$w.is(".edit-colors-window, .storage-manager, .attributes-window, .flip-and-rotate, .stretch-and-skew")) {
return; return;
} }
@ -36,13 +36,13 @@ function make_window_supporting_scale(options) {
// requestAnimationFrame(scale_for_eye_gaze_mode_and_center); // requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
}; };
if(!options.$component){ if (!options.$component) {
$w.center(); $w.center();
const scale_for_eye_gaze_mode_and_center_next_frame = ()=> { const scale_for_eye_gaze_mode_and_center_next_frame = () => {
requestAnimationFrame(scale_for_eye_gaze_mode_and_center); requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
}; };
const on_close = ()=> { const on_close = () => {
$w.off("close", on_close); $w.off("close", on_close);
$G.off("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame); $G.off("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
}; };
@ -57,7 +57,7 @@ function make_window_supporting_scale(options) {
contain: "none", contain: "none",
}); });
} }
return $w; return $w;
} }

View File

@ -7,7 +7,7 @@ function Handles(options) {
const get_ghost_offset_left = options.get_ghost_offset_left || (() => 0); const get_ghost_offset_left = options.get_ghost_offset_left || (() => 0);
const get_ghost_offset_top = options.get_ghost_offset_top || (() => 0); const get_ghost_offset_top = options.get_ghost_offset_top || (() => 0);
const size_only = options.size_only || false; const size_only = options.size_only || false;
const HANDLE_MIDDLE = 0; const HANDLE_MIDDLE = 0;
const HANDLE_START = -1; const HANDLE_START = -1;
const HANDLE_END = 1; const HANDLE_END = 1;
@ -39,63 +39,63 @@ function Handles(options) {
} }
$h.css("touch-action", "none"); $h.css("touch-action", "none");
let rect; let rect;
let dragged = false; let dragged = false;
const resizes_height = y_axis !== HANDLE_MIDDLE; const resizes_height = y_axis !== HANDLE_MIDDLE;
const resizes_width = x_axis !== HANDLE_MIDDLE; const resizes_width = x_axis !== HANDLE_MIDDLE;
if(size_only && (y_axis === HANDLE_TOP || x_axis === HANDLE_LEFT)){ if (size_only && (y_axis === HANDLE_TOP || x_axis === HANDLE_LEFT)) {
$h.addClass("useless-handle"); $h.addClass("useless-handle");
$grab_region.remove(); $grab_region.remove();
}else{ } else {
let cursor_fname; let cursor_fname;
if((x_axis === HANDLE_LEFT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_RIGHT && y_axis === HANDLE_BOTTOM)){ if ((x_axis === HANDLE_LEFT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_RIGHT && y_axis === HANDLE_BOTTOM)) {
cursor_fname = "nwse-resize"; cursor_fname = "nwse-resize";
}else if((x_axis === HANDLE_RIGHT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_LEFT && y_axis === HANDLE_BOTTOM)){ } else if ((x_axis === HANDLE_RIGHT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_LEFT && y_axis === HANDLE_BOTTOM)) {
cursor_fname = "nesw-resize"; cursor_fname = "nesw-resize";
}else if(resizes_width){ } else if (resizes_width) {
cursor_fname = "ew-resize"; cursor_fname = "ew-resize";
}else if(resizes_height){ } else if (resizes_height) {
cursor_fname = "ns-resize"; cursor_fname = "ns-resize";
} }
let fallback_cursor = ""; let fallback_cursor = "";
if(y_axis === HANDLE_TOP){ fallback_cursor += "n"; } if (y_axis === HANDLE_TOP) { fallback_cursor += "n"; }
if(y_axis === HANDLE_BOTTOM){ fallback_cursor += "s"; } if (y_axis === HANDLE_BOTTOM) { fallback_cursor += "s"; }
if(x_axis === HANDLE_LEFT){ fallback_cursor += "w"; } if (x_axis === HANDLE_LEFT) { fallback_cursor += "w"; }
if(x_axis === HANDLE_RIGHT){ fallback_cursor += "e"; } if (x_axis === HANDLE_RIGHT) { fallback_cursor += "e"; }
fallback_cursor += "-resize"; fallback_cursor += "-resize";
const cursor = make_css_cursor(cursor_fname, [16, 16], fallback_cursor); const cursor = make_css_cursor(cursor_fname, [16, 16], fallback_cursor);
$h.add($grab_region).css({cursor}); $h.add($grab_region).css({ cursor });
const drag = (event) => { const drag = (event) => {
$resize_ghost.appendTo($object_container); $resize_ghost.appendTo($object_container);
dragged = true; dragged = true;
rect = options.get_rect(); rect = options.get_rect();
const m = to_canvas_coords(event); const m = to_canvas_coords(event);
let delta_x = 0; let delta_x = 0;
let delta_y = 0; let delta_y = 0;
let width, height; let width, height;
// @TODO: decide between Math.floor/Math.ceil/Math.round for these values // @TODO: decide between Math.floor/Math.ceil/Math.round for these values
if(x_axis === HANDLE_RIGHT){ if (x_axis === HANDLE_RIGHT) {
delta_x = 0; delta_x = 0;
width = ~~(m.x - rect.x); width = ~~(m.x - rect.x);
}else if(x_axis === HANDLE_LEFT){ } else if (x_axis === HANDLE_LEFT) {
delta_x = ~~(m.x - rect.x); delta_x = ~~(m.x - rect.x);
width = ~~(rect.x + rect.width - m.x); width = ~~(rect.x + rect.width - m.x);
}else{ } else {
width = ~~(rect.width); width = ~~(rect.width);
} }
if(y_axis === HANDLE_BOTTOM){ if (y_axis === HANDLE_BOTTOM) {
delta_y = 0; delta_y = 0;
height = ~~(m.y - rect.y); height = ~~(m.y - rect.y);
}else if(y_axis === HANDLE_TOP){ } else if (y_axis === HANDLE_TOP) {
delta_y = ~~(m.y - rect.y); delta_y = ~~(m.y - rect.y);
height = ~~(rect.y + rect.height - m.y); height = ~~(rect.y + rect.height - m.y);
}else{ } else {
height = ~~(rect.height); height = ~~(rect.height);
} }
let new_rect = { let new_rect = {
@ -126,16 +126,16 @@ function Handles(options) {
}; };
$h.add($grab_region).on("pointerdown", event => { $h.add($grab_region).on("pointerdown", event => {
dragged = false; dragged = false;
if(event.button === 0){ if (event.button === 0) {
$G.on("pointermove", drag); $G.on("pointermove", drag);
$("body").css({cursor}).addClass("cursor-bully"); $("body").css({ cursor }).addClass("cursor-bully");
} }
$G.one("pointerup", ()=> { $G.one("pointerup", () => {
$G.off("pointermove", drag); $G.off("pointermove", drag);
$("body").css({cursor: ""}).removeClass("cursor-bully"); $("body").css({ cursor: "" }).removeClass("cursor-bully");
$resize_ghost.remove(); $resize_ghost.remove();
if(dragged){ if (dragged) {
options.set_rect(rect); options.set_rect(rect);
} }
$handles_container.trigger("update"); $handles_container.trigger("update");
@ -145,7 +145,7 @@ function Handles(options) {
event.preventDefault(); event.preventDefault();
}); });
} }
const update_handle = () => { const update_handle = () => {
const rect = options.get_rect(); const rect = options.get_rect();
const hs = $h.width(); const hs = $h.width();
@ -154,14 +154,14 @@ function Handles(options) {
const x = get_handles_offset_left(); const x = get_handles_offset_left();
const y = get_handles_offset_top(); const y = get_handles_offset_top();
const grab_size = 32; const grab_size = 32;
for ({len_key, pos_key, region, offset} of [ for ({ len_key, pos_key, region, offset } of [
{len_key: "width", pos_key: "left", region: x_axis, offset: x}, { len_key: "width", pos_key: "left", region: x_axis, offset: x },
{len_key: "height", pos_key: "top", region: y_axis, offset: y}, { len_key: "height", pos_key: "top", region: y_axis, offset: y },
]) { ]) {
let middle_start = Math.max( let middle_start = Math.max(
rect[len_key] * magnification / 2 - grab_size/2, rect[len_key] * magnification / 2 - grab_size / 2,
Math.min( Math.min(
grab_size/2, grab_size / 2,
rect[len_key] * magnification / 3 rect[len_key] * magnification / 3
) )
); );
@ -171,9 +171,9 @@ function Handles(options) {
middle_start = 0; middle_start = 0;
middle_end = magnification; middle_end = magnification;
} }
const start_start = -grab_size/2; const start_start = -grab_size / 2;
const start_end = Math.min( const start_end = Math.min(
grab_size/2, grab_size / 2,
middle_start middle_start
); );
const end_start = rect[len_key] * magnification - start_end; const end_start = rect[len_key] * magnification - start_end;
@ -197,7 +197,7 @@ function Handles(options) {
[len_key]: middle_end - middle_start, [len_key]: middle_end - middle_start,
}); });
} else if (region === HANDLE_END) { } else if (region === HANDLE_END) {
$h.css({ [pos_key]: offset + (rect[len_key] * magnification - hs/2) }); $h.css({ [pos_key]: offset + (rect[len_key] * magnification - hs / 2) });
$grab_region.css({ $grab_region.css({
[pos_key]: offset + end_start, [pos_key]: offset + end_start,
[len_key]: end_end - end_start, [len_key]: end_end - end_start,
@ -205,17 +205,17 @@ function Handles(options) {
} }
} }
}; };
$handles_container.on("update resize scroll", update_handle); $handles_container.on("update resize scroll", update_handle);
$G.on("resize theme-load", update_handle); $G.on("resize theme-load", update_handle);
setTimeout(update_handle, 50); setTimeout(update_handle, 50);
handles.push($h[0], $grab_region[0]); handles.push($h[0], $grab_region[0]);
}); });
this.handles = handles; this.handles = handles;
// It shouldn't scroll when hiding/showing handles, so don't use jQuery hide/show or CSS display. // It shouldn't scroll when hiding/showing handles, so don't use jQuery hide/show or CSS display.
this.hide = ()=> { $(handles).css({opacity: 0, pointerEvents: "none"}); }; this.hide = () => { $(handles).css({ opacity: 0, pointerEvents: "none" }); };
this.show = ()=> { $(handles).css({opacity: "", pointerEvents: ""}); }; this.show = () => { $(handles).css({ opacity: "", pointerEvents: "" }); };
} }

View File

@ -1,7 +1,7 @@
class OnCanvasHelperLayer extends OnCanvasObject { class OnCanvasHelperLayer extends OnCanvasObject {
constructor(x, y, width, height, hideMainCanvasHandles, pixelRatio = 1) { constructor(x, y, width, height, hideMainCanvasHandles, pixelRatio = 1) {
super(x, y, width, height, hideMainCanvasHandles); super(x, y, width, height, hideMainCanvasHandles);
this.$el.addClass("helper-layer"); this.$el.addClass("helper-layer");
this.$el.css({ this.$el.css({
pointerEvents: "none", pointerEvents: "none",

View File

@ -10,7 +10,7 @@ class OnCanvasObject {
if (this.hideMainCanvasHandles) { if (this.hideMainCanvasHandles) {
canvas_handles.hide(); canvas_handles.hide();
} }
$G.on("resize theme-load", this._global_resize_handler = ()=> { $G.on("resize theme-load", this._global_resize_handler = () => {
this.position(); this.position();
}); });
} }

View File

@ -32,7 +32,7 @@ class OnCanvasSelection extends OnCanvasObject {
}); });
this.position(); this.position();
const instantiate = ()=> { const instantiate = () => {
if (img_or_canvas) { if (img_or_canvas) {
// (this applies when pasting a selection) // (this applies when pasting a selection)
// NOTE: need to create a Canvas because something about imgs makes dragging not work with magnification // NOTE: need to create a Canvas because something about imgs makes dragging not work with magnification
@ -60,13 +60,13 @@ class OnCanvasSelection extends OnCanvasObject {
$handles_container: this.$el, $handles_container: this.$el,
$object_container: $canvas_area, $object_container: $canvas_area,
outset: 2, outset: 2,
get_rect: ()=> ({x: this.x, y: this.y, width: this.width, height: this.height}), get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }),
set_rect: ({x, y, width, height}) => { set_rect: ({ x, y, width, height }) => {
undoable({ undoable({
name: "Resize Selection", name: "Resize Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)), icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true, soft: true,
}, ()=> { }, () => {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.width = width; this.width = width;
@ -75,20 +75,20 @@ class OnCanvasSelection extends OnCanvasObject {
this.resize(); this.resize();
}); });
}, },
get_ghost_offset_left: ()=> parseFloat($canvas_area.css("padding-left")) + 1, get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1, get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
}); });
let mox, moy; let mox, moy;
const pointermove = e => { const pointermove = e => {
make_or_update_undoable({ make_or_update_undoable({
match: (history_node)=> match: (history_node) =>
(e.shiftKey && history_node.name.match(/^(Smear|Stamp|Move) Selection$/)) || (e.shiftKey && history_node.name.match(/^(Smear|Stamp|Move) Selection$/)) ||
(!e.shiftKey && history_node.name.match(/^Move Selection$/)), (!e.shiftKey && history_node.name.match(/^Move Selection$/)),
name: e.shiftKey ? "Smear Selection" : "Move Selection", name: e.shiftKey ? "Smear Selection" : "Move Selection",
update_name: true, update_name: true,
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)), icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true, soft: true,
}, ()=> { }, () => {
const m = to_canvas_coords(e); const m = to_canvas_coords(e);
this.x = Math.max(Math.min(m.x - mox, main_canvas.width), -this.width); this.x = Math.max(Math.min(m.x - mox, main_canvas.width), -this.width);
this.y = Math.max(Math.min(m.y - moy, main_canvas.height), -this.height); this.y = Math.max(Math.min(m.y - moy, main_canvas.height), -this.height);
@ -120,7 +120,7 @@ class OnCanvasSelection extends OnCanvasObject {
name: "Stamp Selection", name: "Stamp Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)), icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true, soft: true,
}, ()=> { }, () => {
this.draw(); this.draw();
}); });
} }
@ -131,7 +131,7 @@ class OnCanvasSelection extends OnCanvasObject {
name: "Stamp Selection", name: "Stamp Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)), icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true, soft: true,
}, ()=> { }, () => {
this.draw(); this.draw();
}); });
} }
@ -141,7 +141,7 @@ class OnCanvasSelection extends OnCanvasObject {
$status_position.text(""); $status_position.text("");
$status_size.text(""); $status_size.text("");
}; };
instantiate(); instantiate();
} }
cut_out_background() { cut_out_background() {
@ -216,10 +216,10 @@ class OnCanvasSelection extends OnCanvasObject {
// @FIXME: work with transparent selected background color // @FIXME: work with transparent selected background color
// (support treating partially transparent background colors as transparency) // (support treating partially transparent background colors as transparency)
if ( if (
Math.abs(sourceImageData.data[i+0] - background_color_rgba[0]) <= match_threshold && Math.abs(sourceImageData.data[i + 0] - background_color_rgba[0]) <= match_threshold &&
Math.abs(sourceImageData.data[i+1] - background_color_rgba[1]) <= match_threshold && Math.abs(sourceImageData.data[i + 1] - background_color_rgba[1]) <= match_threshold &&
Math.abs(sourceImageData.data[i+2] - background_color_rgba[2]) <= match_threshold && Math.abs(sourceImageData.data[i + 2] - background_color_rgba[2]) <= match_threshold &&
Math.abs(sourceImageData.data[i+3] - background_color_rgba[3]) <= match_threshold Math.abs(sourceImageData.data[i + 3] - background_color_rgba[3]) <= match_threshold
) { ) {
in_cutout = false; in_cutout = false;
} }

View File

@ -12,7 +12,7 @@ class OnCanvasTextBox extends OnCanvasObject {
foreignObject.setAttribute("x", 0); foreignObject.setAttribute("x", 0);
foreignObject.setAttribute("y", 0); foreignObject.setAttribute("y", 0);
svg.append(foreignObject); svg.append(foreignObject);
// inline styles so that they'll be serialized for the SVG // inline styles so that they'll be serialized for the SVG
this.$editor.css({ this.$editor.css({
position: "absolute", position: "absolute",
@ -37,7 +37,7 @@ class OnCanvasTextBox extends OnCanvasObject {
this.canvas.style.pointerEvents = "none"; this.canvas.style.pointerEvents = "none";
this.$el.append(this.canvas); this.$el.append(this.canvas);
const update_size = ()=> { const update_size = () => {
this.position(); this.position();
this.$el.triggerHandler("update"); // update handles this.$el.triggerHandler("update"); // update handles
this.$editor.add(render_textarea).css({ this.$editor.add(render_textarea).css({
@ -46,7 +46,7 @@ class OnCanvasTextBox extends OnCanvasObject {
}); });
}; };
const auto_size = ()=> { const auto_size = () => {
// Auto-expand, and apply minimum size. // Auto-expand, and apply minimum size.
edit_textarea.style.height = ""; edit_textarea.style.height = "";
edit_textarea.style.minHeight = "0px"; edit_textarea.style.minHeight = "0px";
@ -60,13 +60,13 @@ class OnCanvasTextBox extends OnCanvasObject {
update_size(); update_size();
}; };
const update = ()=> { const update = () => {
requestAnimationFrame(()=> { requestAnimationFrame(() => {
edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync
}); });
const font = text_tool_font; const font = text_tool_font;
const get_solid_color = (swatch)=> `rgba(${get_rgba_from_color(swatch).join(", ")}`; const get_solid_color = (swatch) => `rgba(${get_rgba_from_color(swatch).join(", ")}`;
font.color = get_solid_color(selected_colors.foreground); font.color = get_solid_color(selected_colors.foreground);
font.background = tool_transparent_mode ? "transparent" : get_solid_color(selected_colors.background); font.background = tool_transparent_mode ? "transparent" : get_solid_color(selected_colors.background);
this.$editor.add(this.canvas).css({ this.$editor.add(this.canvas).css({
@ -96,7 +96,7 @@ class OnCanvasTextBox extends OnCanvasObject {
render_textarea.removeChild(render_textarea.firstChild); render_textarea.removeChild(render_textarea.firstChild);
} }
render_textarea.appendChild(document.createTextNode(edit_textarea.value)); render_textarea.appendChild(document.createTextNode(edit_textarea.value));
svg.setAttribute("width", this.width); svg.setAttribute("width", this.width);
svg.setAttribute("height", this.height); svg.setAttribute("height", this.height);
foreignObject.setAttribute("width", this.width); foreignObject.setAttribute("width", this.width);
@ -104,15 +104,15 @@ class OnCanvasTextBox extends OnCanvasObject {
var svg_source = new XMLSerializer().serializeToString(svg); var svg_source = new XMLSerializer().serializeToString(svg);
var data_url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg_source)}`; var data_url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg_source)}`;
var img = new Image(); var img = new Image();
img.onload = ()=> { img.onload = () => {
this.canvas.width = this.width; this.canvas.width = this.width;
this.canvas.height = this.height; this.canvas.height = this.height;
this.canvas.ctx.drawImage(img, 0, 0); this.canvas.ctx.drawImage(img, 0, 0);
update_helper_layer(); // @TODO: under-grid specific helper layer? update_helper_layer(); // @TODO: under-grid specific helper layer?
}; };
img.onerror = (event)=> { img.onerror = (event) => {
window.console && console.log("Failed to load image", event); window.console && console.log("Failed to load image", event);
}; };
img.src = data_url; img.src = data_url;
@ -120,8 +120,8 @@ class OnCanvasTextBox extends OnCanvasObject {
$G.on("option-changed", this._on_option_changed = update); $G.on("option-changed", this._on_option_changed = update);
this.$editor.on("input", this._on_input = update); this.$editor.on("input", this._on_input = update);
this.$editor.on("scroll", this._on_scroll = ()=> { this.$editor.on("scroll", this._on_scroll = () => {
requestAnimationFrame(()=> { requestAnimationFrame(() => {
edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync
}); });
}); });
@ -131,7 +131,7 @@ class OnCanvasTextBox extends OnCanvasObject {
touchAction: "none", touchAction: "none",
}); });
this.position(); this.position();
this.$el.append(this.$editor); this.$el.append(this.$editor);
this.$editor[0].focus(); this.$editor[0].focus();
this.handles = new Handles({ this.handles = new Handles({
@ -139,8 +139,8 @@ class OnCanvasTextBox extends OnCanvasObject {
$object_container: $canvas_area, $object_container: $canvas_area,
outset: 2, outset: 2,
thick: true, thick: true,
get_rect: ()=> ({x: this.x, y: this.y, width: this.width, height: this.height}), get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }),
set_rect: ({x, y, width, height}) => { set_rect: ({ x, y, width, height }) => {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.width = width; this.width = width;
@ -152,7 +152,7 @@ class OnCanvasTextBox extends OnCanvasObject {
this.canvas.width = width; this.canvas.width = width;
this.canvas.height = height; this.canvas.height = height;
}, },
constrain_rect: ({x, y, width, height}, x_axis, y_axis)=> { constrain_rect: ({ x, y, width, height }, x_axis, y_axis) => {
// remember dimensions // remember dimensions
const old_x = this.x; const old_x = this.x;
const old_y = this.y; const old_y = this.y;
@ -176,7 +176,7 @@ class OnCanvasTextBox extends OnCanvasObject {
if (y_axis === -1) { if (y_axis === -1) {
y = Math.min(this.y, old_y + old_height - this.height); y = Math.min(this.y, old_y + old_height - this.height);
} }
// remember constrained dimensions // remember constrained dimensions
width = this.width; width = this.width;
height = this.height; height = this.height;
@ -188,10 +188,10 @@ class OnCanvasTextBox extends OnCanvasObject {
this.height = old_height; this.height = old_height;
update_size(); update_size();
return {x, y, width, height}; return { x, y, width, height };
}, },
get_ghost_offset_left: ()=> parseFloat($canvas_area.css("padding-left")) + 1, get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1, get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
}); });
let mox, moy; // mouse offset let mox, moy; // mouse offset
const pointermove = e => { const pointermove = e => {
@ -234,7 +234,7 @@ class OnCanvasTextBox extends OnCanvasObject {
OnCanvasTextBox.$fontbox = null; OnCanvasTextBox.$fontbox = null;
} }
const $fb = OnCanvasTextBox.$fontbox = OnCanvasTextBox.$fontbox || new $FontBox(); const $fb = OnCanvasTextBox.$fontbox = OnCanvasTextBox.$fontbox || new $FontBox();
const displace_font_box = ()=> { const displace_font_box = () => {
// move the font box out of the way if it's overlapping the OnCanvasTextBox // move the font box out of the way if it's overlapping the OnCanvasTextBox
const fb_rect = $fb[0].getBoundingClientRect(); const fb_rect = $fb[0].getBoundingClientRect();
const tb_rect = this.$el[0].getBoundingClientRect(); const tb_rect = this.$el[0].getBoundingClientRect();
@ -257,12 +257,12 @@ class OnCanvasTextBox extends OnCanvasObject {
update(); update();
displace_font_box(); displace_font_box();
// In case a software keyboard opens, like Optikey for eye gaze / head tracking users, // In case a software keyboard opens, like Optikey for eye gaze / head tracking users,
// or perhaps a handwriting input for pen tablet users, or *partially* for mobile browsers. // or perhaps a handwriting input for pen tablet users, or *partially* for mobile browsers.
// Mobile browsers generally scroll the view for a textbox well enough, but // Mobile browsers generally scroll the view for a textbox well enough, but
// don't include the custom behavior of moving the font box out of the way. // don't include the custom behavior of moving the font box out of the way.
$(window).on("resize", this._on_window_resize = ()=> { $(window).on("resize", this._on_window_resize = () => {
this.$editor[0].scrollIntoView({ block: 'nearest', inline: 'nearest' }); this.$editor[0].scrollIntoView({ block: 'nearest', inline: 'nearest' });
displace_font_box(); displace_font_box();
}); });

View File

@ -25,7 +25,7 @@ function get_hotkey(text) {
} }
let localizations = {}; let localizations = {};
window.localize = (english_text, ...interpolations)=> { window.localize = (english_text, ...interpolations) => {
function find_localization(english_text) { function find_localization(english_text) {
const amp_index = index_of_hotkey(english_text); const amp_index = index_of_hotkey(english_text);
if (amp_index > -1) { if (amp_index > -1) {
@ -51,7 +51,7 @@ window.localize = (english_text, ...interpolations)=> {
} }
function interpolate(text, interpolations) { function interpolate(text, interpolations) {
for (let i = 0; i < interpolations.length; i++) { for (let i = 0; i < interpolations.length; i++) {
text = text.replace(`%${i+1}`, interpolations[i]); text = text.replace(`%${i + 1}`, interpolations[i]);
} }
return text; return text;
} }
@ -1038,7 +1038,7 @@ for (const accepted_language of accepted_languages) {
function get_language() { function get_language() {
return current_language; return current_language;
} }
function get_direction(language=current_language) { function get_direction(language = current_language) {
return language.match(/^(ar|dv|fa|ha|he|ks|ku|ms|pa|ps|sd|ug|yi)\b/i) ? "rtl" : "ltr"; return language.match(/^(ar|dv|fa|ha|he|ks|ku|ms|pa|ps|sd|ug|yi)\b/i) ? "rtl" : "ltr";
} }
function load_language(language) { function load_language(language) {
@ -1059,8 +1059,8 @@ function load_language(language) {
stylesheet.setAttribute("href", href); stylesheet.setAttribute("href", href);
// hack to wait for stylesheet to load // hack to wait for stylesheet to load
const img = document.createElement("img"); const img = document.createElement("img");
img.onerror = ()=> { img.onerror = () => {
$(()=> { $(() => {
$G.triggerHandler("theme-load"); // signal layout change $G.triggerHandler("theme-load"); // signal layout change
}); });
}; };

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@ try {
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (error) { } } catch (error) { }
if (dev_edit_colors) { if (dev_edit_colors) {
$(()=> { $(() => {
show_edit_colors_window(); show_edit_colors_window();
$(".define-custom-colors-button").click(); $(".define-custom-colors-button").click();
$edit_colors_window.css({ $edit_colors_window.css({
@ -58,7 +58,7 @@ function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit)
const $palette = $swatch_to_edit.closest(".palette, .color-box"); const $palette = $swatch_to_edit.closest(".palette, .color-box");
const swatch_index = $palette.find(".swatch").toArray().indexOf($swatch_to_edit[0]); const swatch_index = $palette.find(".swatch").toArray().indexOf($swatch_to_edit[0]);
const initial_color = selected_colors[color_selection_slot_to_edit]; const initial_color = selected_colors[color_selection_slot_to_edit];
choose_color(initial_color, (color)=> { choose_color(initial_color, (color) => {
// The palette may have changed or rerendered due to switching themes, // The palette may have changed or rerendered due to switching themes,
// toggling vertical color box mode, or monochrome document mode. // toggling vertical color box mode, or monochrome document mode.
$swatch_to_edit = $($palette.find(".swatch")[swatch_index]); $swatch_to_edit = $($palette.find(".swatch")[swatch_index]);
@ -77,15 +77,15 @@ function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit)
const selection_monochrome_info = (selection && selection.canvas) ? detect_monochrome(selection.canvas.ctx) : main_monochrome_info; const selection_monochrome_info = (selection && selection.canvas) ? detect_monochrome(selection.canvas.ctx) : main_monochrome_info;
const selection_matches_main_canvas_colors = const selection_matches_main_canvas_colors =
selection_monochrome_info.isMonochrome && selection_monochrome_info.isMonochrome &&
selection_monochrome_info.presentNonTransparentRGBAs.every((rgba)=> selection_monochrome_info.presentNonTransparentRGBAs.every((rgba) =>
main_monochrome_info.presentNonTransparentRGBAs.map(rgba=> rgba.toString()).includes(rgba.toString()) main_monochrome_info.presentNonTransparentRGBAs.map(rgba => rgba.toString()).includes(rgba.toString())
); );
if ( if (
main_monochrome_info.isMonochrome && main_monochrome_info.isMonochrome &&
selection_monochrome_info.isMonochrome && selection_monochrome_info.isMonochrome &&
selection_matches_main_canvas_colors selection_matches_main_canvas_colors
) { ) {
const recolor = (ctx, present_rgbas)=> { const recolor = (ctx, present_rgbas) => {
// HTML5 Canvas API is unreliable for exact colors. // HTML5 Canvas API is unreliable for exact colors.
// 1. The specifications specify unpremultiplied alpha, but in practice browsers use premultiplied alpha for performance. // 1. The specifications specify unpremultiplied alpha, but in practice browsers use premultiplied alpha for performance.
// 2. Some browsers implement protections against fingerprinting that return slightly random data // 2. Some browsers implement protections against fingerprinting that return slightly random data
@ -94,10 +94,10 @@ function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit)
// Some global system color profile might apply however, I don't know how all that works.) // Some global system color profile might apply however, I don't know how all that works.)
if ( if (
present_rgbas.length === 2 && present_rgbas.length === 2 &&
present_rgbas.every((present_rgba)=> `${present_rgba}` !== `${old_rgba}`) present_rgbas.every((present_rgba) => `${present_rgba}` !== `${old_rgba}`)
) { ) {
// Find the nearer color in the image data to replace. // Find the nearer color in the image data to replace.
const distances = present_rgbas.map((rgba)=> const distances = present_rgbas.map((rgba) =>
Math.abs(rgba[0] - old_rgba[0]) + Math.abs(rgba[0] - old_rgba[0]) +
Math.abs(rgba[1] - old_rgba[1]) + Math.abs(rgba[1] - old_rgba[1]) +
Math.abs(rgba[2] - old_rgba[2]) + Math.abs(rgba[2] - old_rgba[2]) +
@ -116,7 +116,7 @@ function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit)
undoable({ undoable({
name: "Recolor", name: "Recolor",
icon: get_help_folder_icon("p_color.png"), icon: get_help_folder_icon("p_color.png"),
}, ()=> { }, () => {
recolor(main_ctx, main_monochrome_info.presentNonTransparentRGBAs); recolor(main_ctx, main_monochrome_info.presentNonTransparentRGBAs);
if (selection && selection.canvas) { if (selection && selection.canvas) {
recolor(selection.canvas.ctx, selection_monochrome_info.presentNonTransparentRGBAs); recolor(selection.canvas.ctx, selection_monochrome_info.presentNonTransparentRGBAs);
@ -140,7 +140,7 @@ function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit)
update_$swatch($swatch_to_edit, color); update_$swatch($swatch_to_edit, color);
selected_colors[color_selection_slot_to_edit] = color; selected_colors[color_selection_slot_to_edit] = color;
$G.triggerHandler("option-changed"); $G.triggerHandler("option-changed");
window.console && console.log(`Updated palette: ${palette.map(()=> `%c█`).join("")}`, ...palette.map((color)=> `color: ${color};`)); window.console && console.log(`Updated palette: ${palette.map(() => `%c█`).join("")}`, ...palette.map((color) => `color: ${color};`));
} }
}); });
} }
@ -160,18 +160,18 @@ function choose_color(initial_color, callback) {
let custom_colors_index = 0; let custom_colors_index = 0;
const get_current_color = ()=> `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`; const get_current_color = () => `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`;
const set_color_from_rgb = (r, g, b)=> { const set_color_from_rgb = (r, g, b) => {
const [h, s, l] = rgb_to_hsl(r, g, b); const [h, s, l] = rgb_to_hsl(r, g, b);
hue_degrees = h * 360; hue_degrees = h * 360;
sat_percent = s * 100; sat_percent = s * 100;
lum_percent = l * 100; lum_percent = l * 100;
}; };
const set_color = (color)=> { const set_color = (color) => {
const [r, g, b] = get_rgba_from_color(color); const [r, g, b] = get_rgba_from_color(color);
set_color_from_rgb(r, g, b); set_color_from_rgb(r, g, b);
}; };
const select = ($swatch)=> { const select = ($swatch) => {
$w.$content.find(".swatch").removeClass("selected"); $w.$content.find(".swatch").removeClass("selected");
$swatch.addClass("selected"); $swatch.addClass("selected");
set_color($swatch[0].dataset.color); set_color($swatch[0].dataset.color);
@ -183,8 +183,8 @@ function choose_color(initial_color, callback) {
update_inputs("hslrgb"); update_inputs("hslrgb");
}; };
const make_color_grid = (colors, id)=> { const make_color_grid = (colors, id) => {
const $color_grid = $(`<div class="color-grid" tabindex="0">`).attr({id}); const $color_grid = $(`<div class="color-grid" tabindex="0">`).attr({ id });
for (const color of colors) { for (const color of colors) {
const $swatch = $Swatch(color); const $swatch = $Swatch(color);
$swatch.appendTo($color_grid).addClass("inset-deep"); $swatch.appendTo($color_grid).addClass("inset-deep");
@ -192,7 +192,7 @@ function choose_color(initial_color, callback) {
} }
let $local_last_focus = $color_grid.find(".swatch:first-child"); let $local_last_focus = $color_grid.find(".swatch:first-child");
const num_colors_per_row = 8; const num_colors_per_row = 8;
const navigate = (relative_index)=> { const navigate = (relative_index) => {
const $focused = $color_grid.find(".swatch:focus"); const $focused = $color_grid.find(".swatch:focus");
if (!$focused.length) { return; } if (!$focused.length) { return; }
const $swatches = $color_grid.find(".swatch"); const $swatches = $color_grid.find(".swatch");
@ -205,7 +205,7 @@ function choose_color(initial_color, callback) {
if (!$to_focus.length) { return; } if (!$to_focus.length) { return; }
$to_focus.focus(); $to_focus.focus();
}; };
$color_grid.on("keydown", (event)=> { $color_grid.on("keydown", (event) => {
// console.log(event.code); // console.log(event.code);
if (event.code === "ArrowRight") { navigate(+1); } if (event.code === "ArrowRight") { navigate(+1); }
if (event.code === "ArrowLeft") { navigate(-1); } if (event.code === "ArrowLeft") { navigate(-1); }
@ -218,17 +218,17 @@ function choose_color(initial_color, callback) {
draw(); draw();
} }
}); });
$color_grid.on("pointerdown", (event)=> { $color_grid.on("pointerdown", (event) => {
const $swatch = $(event.target).closest(".swatch"); const $swatch = $(event.target).closest(".swatch");
if ($swatch.length) { if ($swatch.length) {
select($swatch); select($swatch);
draw(); draw();
} }
}); });
$color_grid.on("dragstart", (event)=> { $color_grid.on("dragstart", (event) => {
event.preventDefault(); event.preventDefault();
}); });
$color_grid.on("focusin", (event)=> { $color_grid.on("focusin", (event) => {
if (event.target.closest(".swatch")) { if (event.target.closest(".swatch")) {
$local_last_focus = $(event.target.closest(".swatch")); $local_last_focus = $(event.target.closest(".swatch"));
} else { } else {
@ -241,7 +241,7 @@ function choose_color(initial_color, callback) {
// since the parent grid is previous in the tab order // since the parent grid is previous in the tab order
$color_grid.attr("tabindex", -1); $color_grid.attr("tabindex", -1);
}); });
$color_grid.on("focusout", (event)=> { $color_grid.on("focusout", (event) => {
$color_grid.attr("tabindex", 0); $color_grid.attr("tabindex", 0);
}); });
return $color_grid; return $color_grid;
@ -271,26 +271,26 @@ function choose_color(initial_color, callback) {
} }
const $define_custom_colors_button = $(`<button class="define-custom-colors-button">`) const $define_custom_colors_button = $(`<button class="define-custom-colors-button">`)
.html(display_hotkey("&Define Custom Colors >>")) .html(display_hotkey("&Define Custom Colors >>"))
.appendTo($left) .appendTo($left)
.on("click", (e)=> { .on("click", (e) => {
// prevent the form from submitting // prevent the form from submitting
// @TODO: instead, prevent the form's submit event in $Window.js in os-gui (or don't have a form? idk) // @TODO: instead, prevent the form's submit event in $Window.js in os-gui (or don't have a form? idk)
e.preventDefault(); e.preventDefault();
$right.show(); $right.show();
$w.addClass("defining-custom-colors"); // for mobile layout $w.addClass("defining-custom-colors"); // for mobile layout
$define_custom_colors_button.attr("disabled", "disabled"); $define_custom_colors_button.attr("disabled", "disabled");
// assuming small viewport implies mobile implies an onscreen keyboard, // assuming small viewport implies mobile implies an onscreen keyboard,
// and that you probably don't want to use the keyboard to choose colors // and that you probably don't want to use the keyboard to choose colors
if ($w.width() >= 300) { if ($w.width() >= 300) {
inputs_by_component_letter.h.focus(); inputs_by_component_letter.h.focus();
} }
maybe_reenable_button_for_mobile_navigation(); maybe_reenable_button_for_mobile_navigation();
}); });
// for mobile layout, re-enable button because it's a navigation button in that case, rather than one-time expand action // for mobile layout, re-enable button because it's a navigation button in that case, rather than one-time expand action
const maybe_reenable_button_for_mobile_navigation = ()=> { const maybe_reenable_button_for_mobile_navigation = () => {
// if ($right.is(":hidden")) { // if ($right.is(":hidden")) {
if ($w.width() < 300 || document.body.classList.contains("eye-gaze-mode")) { if ($w.width() < 300 || document.body.classList.contains("eye-gaze-mode")) {
$define_custom_colors_button.removeAttr("disabled"); $define_custom_colors_button.removeAttr("disabled");
@ -315,43 +315,43 @@ function choose_color(initial_color, callback) {
left: 10, left: 10,
top: 198, top: 198,
}); });
let mouse_down_on_rainbow_canvas = false; let mouse_down_on_rainbow_canvas = false;
let crosshair_shown_on_rainbow_canvas = false; let crosshair_shown_on_rainbow_canvas = false;
const draw = ()=> { const draw = () => {
if (!mouse_down_on_rainbow_canvas || crosshair_shown_on_rainbow_canvas) { if (!mouse_down_on_rainbow_canvas || crosshair_shown_on_rainbow_canvas) {
// rainbow // rainbow
for (let y = 0; y < rainbow_canvas.height; y += 6) { for (let y = 0; y < rainbow_canvas.height; y += 6) {
for (let x = -1; x < rainbow_canvas.width; x += 3) { for (let x = -1; x < rainbow_canvas.width; x += 3) {
rainbow_canvas.ctx.fillStyle = `hsl(${x/rainbow_canvas.width*360}deg, ${(1-y/rainbow_canvas.height)*100}%, 50%)`; rainbow_canvas.ctx.fillStyle = `hsl(${x / rainbow_canvas.width * 360}deg, ${(1 - y / rainbow_canvas.height) * 100}%, 50%)`;
rainbow_canvas.ctx.fillRect(x, y, 3, 6); rainbow_canvas.ctx.fillRect(x, y, 3, 6);
} }
} }
// crosshair // crosshair
if (!mouse_down_on_rainbow_canvas) { if (!mouse_down_on_rainbow_canvas) {
const x = ~~(hue_degrees/360*rainbow_canvas.width); const x = ~~(hue_degrees / 360 * rainbow_canvas.width);
const y = ~~((1-sat_percent/100)*rainbow_canvas.height); const y = ~~((1 - sat_percent / 100) * rainbow_canvas.height);
rainbow_canvas.ctx.fillStyle = "black"; rainbow_canvas.ctx.fillStyle = "black";
rainbow_canvas.ctx.fillRect(x-1, y-9, 3, 5); rainbow_canvas.ctx.fillRect(x - 1, y - 9, 3, 5);
rainbow_canvas.ctx.fillRect(x-1, y+5, 3, 5); rainbow_canvas.ctx.fillRect(x - 1, y + 5, 3, 5);
rainbow_canvas.ctx.fillRect(x-9, y-1, 5, 3); rainbow_canvas.ctx.fillRect(x - 9, y - 1, 5, 3);
rainbow_canvas.ctx.fillRect(x+5, y-1, 5, 3); rainbow_canvas.ctx.fillRect(x + 5, y - 1, 5, 3);
} }
crosshair_shown_on_rainbow_canvas = !mouse_down_on_rainbow_canvas; crosshair_shown_on_rainbow_canvas = !mouse_down_on_rainbow_canvas;
} }
for (let y = -2; y < luminosity_canvas.height; y += 6) { for (let y = -2; y < luminosity_canvas.height; y += 6) {
luminosity_canvas.ctx.fillStyle = `hsl(${hue_degrees}deg, ${sat_percent}%, ${(1-y/luminosity_canvas.height)*100}%)`; luminosity_canvas.ctx.fillStyle = `hsl(${hue_degrees}deg, ${sat_percent}%, ${(1 - y / luminosity_canvas.height) * 100}%)`;
luminosity_canvas.ctx.fillRect(0, y, luminosity_canvas.width, 6); luminosity_canvas.ctx.fillRect(0, y, luminosity_canvas.width, 6);
} }
lum_arrow_canvas.ctx.fillStyle = getComputedStyle($w.$content[0]).getPropertyValue("--ButtonText"); lum_arrow_canvas.ctx.fillStyle = getComputedStyle($w.$content[0]).getPropertyValue("--ButtonText");
for (let x = 0; x < lum_arrow_canvas.width; x++) { for (let x = 0; x < lum_arrow_canvas.width; x++) {
lum_arrow_canvas.ctx.fillRect(x, lum_arrow_canvas.width-x-1, 1, 1+x*2); lum_arrow_canvas.ctx.fillRect(x, lum_arrow_canvas.width - x - 1, 1, 1 + x * 2);
} }
lum_arrow_canvas.style.position = "absolute"; lum_arrow_canvas.style.position = "absolute";
lum_arrow_canvas.style.left = "215px"; lum_arrow_canvas.style.left = "215px";
lum_arrow_canvas.style.top = `${3 + ~~((1-lum_percent/100)*luminosity_canvas.height)}px`; lum_arrow_canvas.style.top = `${3 + ~~((1 - lum_percent / 100) * luminosity_canvas.height)}px`;
result_canvas.ctx.fillStyle = get_current_color(); result_canvas.ctx.fillStyle = get_current_color();
result_canvas.ctx.fillRect(0, 0, result_canvas.width, result_canvas.height); result_canvas.ctx.fillRect(0, 0, result_canvas.width, result_canvas.height);
@ -361,52 +361,52 @@ function choose_color(initial_color, callback) {
$(luminosity_canvas).addClass("luminosity-canvas inset-shallow"); $(luminosity_canvas).addClass("luminosity-canvas inset-shallow");
$(result_canvas).addClass("result-color-canvas inset-shallow").attr("id", "color-solid-canvas"); $(result_canvas).addClass("result-color-canvas inset-shallow").attr("id", "color-solid-canvas");
const select_hue_sat = (event)=> { const select_hue_sat = (event) => {
hue_degrees = Math.min(1, Math.max(0, event.offsetX/rainbow_canvas.width))*360; hue_degrees = Math.min(1, Math.max(0, event.offsetX / rainbow_canvas.width)) * 360;
sat_percent = Math.min(1, Math.max(0, (1 - event.offsetY/rainbow_canvas.height)))*100; sat_percent = Math.min(1, Math.max(0, (1 - event.offsetY / rainbow_canvas.height))) * 100;
update_inputs("hsrgb"); update_inputs("hsrgb");
draw(); draw();
event.preventDefault(); event.preventDefault();
}; };
$(rainbow_canvas).on("pointerdown", (event)=> { $(rainbow_canvas).on("pointerdown", (event) => {
mouse_down_on_rainbow_canvas = true; mouse_down_on_rainbow_canvas = true;
select_hue_sat(event); select_hue_sat(event);
$(rainbow_canvas).on("pointermove", select_hue_sat); $(rainbow_canvas).on("pointermove", select_hue_sat);
if (event.pointerId !== 1234567890) { // for Eye Gaze Mode simulated clicks if (event.pointerId !== 1234567890) { // for Eye Gaze Mode simulated clicks
rainbow_canvas.setPointerCapture(event.pointerId); rainbow_canvas.setPointerCapture(event.pointerId);
} }
}); });
$G.on("pointerup pointercancel", (event)=> { $G.on("pointerup pointercancel", (event) => {
$(rainbow_canvas).off("pointermove", select_hue_sat); $(rainbow_canvas).off("pointermove", select_hue_sat);
// rainbow_canvas.releasePointerCapture(event.pointerId); // rainbow_canvas.releasePointerCapture(event.pointerId);
mouse_down_on_rainbow_canvas = false; mouse_down_on_rainbow_canvas = false;
draw(); draw();
}); });
const select_lum = (event)=> { const select_lum = (event) => {
lum_percent = Math.min(1, Math.max(0, (1 - event.offsetY/luminosity_canvas.height)))*100; lum_percent = Math.min(1, Math.max(0, (1 - event.offsetY / luminosity_canvas.height))) * 100;
update_inputs("lrgb"); update_inputs("lrgb");
draw(); draw();
event.preventDefault(); event.preventDefault();
}; };
$(luminosity_canvas).on("pointerdown", (event)=> { $(luminosity_canvas).on("pointerdown", (event) => {
select_lum(event); select_lum(event);
$(luminosity_canvas).on("pointermove", select_lum); $(luminosity_canvas).on("pointermove", select_lum);
if (event.pointerId !== 1234567890) { // for Eye Gaze Mode simulated clicks if (event.pointerId !== 1234567890) { // for Eye Gaze Mode simulated clicks
luminosity_canvas.setPointerCapture(event.pointerId); luminosity_canvas.setPointerCapture(event.pointerId);
} }
}); });
$G.on("pointerup pointercancel", (event)=> { $G.on("pointerup pointercancel", (event) => {
$(luminosity_canvas).off("pointermove", select_lum); $(luminosity_canvas).off("pointermove", select_lum);
// luminosity_canvas.releasePointerCapture(event.pointerId); // luminosity_canvas.releasePointerCapture(event.pointerId);
}); });
const inputs_by_component_letter = {}; const inputs_by_component_letter = {};
["hsl", "rgb"].forEach((color_model, color_model_index)=> { ["hsl", "rgb"].forEach((color_model, color_model_index) => {
[...color_model].forEach((component_letter, component_index)=> { [...color_model].forEach((component_letter, component_index) => {
const text_with_hotkey = { const text_with_hotkey = {
h: "Hu&e:", h: "Hu&e:",
s: "&Sat:", s: "&Sat:",
@ -456,7 +456,7 @@ function choose_color(initial_color, callback) {
}); });
// listening for input events on input elements using event delegation (looks a little weird) // listening for input events on input elements using event delegation (looks a little weird)
$right.on("input", "input", (event)=> { $right.on("input", "input", (event) => {
const input = event.target; const input = event.target;
const component_letter = input.dataset.componentLetter; const component_letter = input.dataset.componentLetter;
if (component_letter) { if (component_letter) {
@ -489,7 +489,7 @@ function choose_color(initial_color, callback) {
update_inputs("rgb"); update_inputs("rgb");
} else { } else {
let [r, g, b] = get_rgba_from_color(get_current_color()); let [r, g, b] = get_rgba_from_color(get_current_color());
const rgb = {r, g, b}; const rgb = { r, g, b };
rgb[component_letter] = n; rgb[component_letter] = n;
set_color_from_rgb(rgb.r, rgb.g, rgb.b); set_color_from_rgb(rgb.r, rgb.g, rgb.b);
update_inputs("hsl"); update_inputs("hsl");
@ -501,7 +501,7 @@ function choose_color(initial_color, callback) {
} }
} }
}); });
$right.on("focusout", "input", (event)=> { $right.on("focusout", "input", (event) => {
const input = event.target; const input = event.target;
const component_letter = input.dataset.componentLetter; const component_letter = input.dataset.componentLetter;
if (component_letter) { if (component_letter) {
@ -513,7 +513,7 @@ function choose_color(initial_color, callback) {
} }
}); });
$w.on("keydown", (event)=> { $w.on("keydown", (event) => {
if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) { if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
switch (event.key) { switch (event.key) {
case "o": case "o":
@ -546,7 +546,7 @@ function choose_color(initial_color, callback) {
inputs_by_component_letter.b.focus(); inputs_by_component_letter.b.focus();
break; break;
case "a": case "a":
if ($add_to_custom_colors_button.is(":visible")) { if ($add_to_custom_colors_button.is(":visible")) {
$add_to_custom_colors_button.click(); $add_to_custom_colors_button.click();
} }
break; break;
@ -563,7 +563,7 @@ function choose_color(initial_color, callback) {
event.stopPropagation(); event.stopPropagation();
}); });
const update_inputs = (components)=> { const update_inputs = (components) => {
for (const component_letter of components) { for (const component_letter of components) {
const input = inputs_by_component_letter[component_letter]; const input = inputs_by_component_letter[component_letter];
const [r, g, b] = get_rgba_from_color(get_current_color()); const [r, g, b] = get_rgba_from_color(get_current_color());
@ -581,21 +581,21 @@ function choose_color(initial_color, callback) {
$right.append(rainbow_canvas, luminosity_canvas, result_canvas, $color_solid_label, lum_arrow_canvas); $right.append(rainbow_canvas, luminosity_canvas, result_canvas, $color_solid_label, lum_arrow_canvas);
const $add_to_custom_colors_button = $(`<button class="add-to-custom-colors-button">`) const $add_to_custom_colors_button = $(`<button class="add-to-custom-colors-button">`)
.html(display_hotkey("&Add To Custom Colors")) .html(display_hotkey("&Add To Custom Colors"))
.appendTo($right) .appendTo($right)
.on("click", (event)=> { .on("click", (event) => {
// prevent the form from submitting // prevent the form from submitting
// @TODO: instead, prevent the form's submit event in $Window.js in os-gui (or don't have a form? idk) // @TODO: instead, prevent the form's submit event in $Window.js in os-gui (or don't have a form? idk)
event.preventDefault(); event.preventDefault();
const color = get_current_color(); const color = get_current_color();
custom_colors[custom_colors_index] = color; custom_colors[custom_colors_index] = color;
// console.log(custom_colors_swatches_reordered, custom_colors_index, custom_colors_swatches_reordered[custom_colors_index])); // console.log(custom_colors_swatches_reordered, custom_colors_index, custom_colors_swatches_reordered[custom_colors_index]));
update_$swatch($(custom_colors_swatches_list_order[custom_colors_index]), color); update_$swatch($(custom_colors_swatches_list_order[custom_colors_index]), color);
custom_colors_index = (custom_colors_index + 1) % custom_colors.length; custom_colors_index = (custom_colors_index + 1) % custom_colors.length;
$w.removeClass("defining-custom-colors"); // for mobile layout $w.removeClass("defining-custom-colors"); // for mobile layout
}); });
$w.$Button(localize("OK"), () => { $w.$Button(localize("OK"), () => {
callback(get_current_color()); callback(get_current_color());
@ -619,7 +619,7 @@ function choose_color(initial_color, callback) {
custom_colors_index = Math.max(0, custom_colors_swatches_list_order.indexOf( custom_colors_index = Math.max(0, custom_colors_swatches_list_order.indexOf(
$custom_colors_grid.find(".swatch.selected")[0] $custom_colors_grid.find(".swatch.selected")[0]
)); ));
set_color(initial_color); set_color(initial_color);
update_inputs("hslrgb"); update_inputs("hslrgb");

View File

@ -51,7 +51,7 @@ async function write_blob_to_file_path(filePath, blob) {
window.systemHooks = window.systemHooks || {}; window.systemHooks = window.systemHooks || {};
window.systemHooks.showSaveFileDialog = async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable }) => { window.systemHooks.showSaveFileDialog = async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable }) => {
// First filter in filters list determines default selected file type. // First filter in filters list determines default selected file type.
// @TODO: default to existing extension, except it would be awkward to rearrange the list... // @TODO: default to existing extension, except it would be awkward to rearrange the list...
// const suggestedExtension = get_file_extension(defaultFileName); // const suggestedExtension = get_file_extension(defaultFileName);

View File

@ -104,13 +104,13 @@ const createWindow = () => {
// Similarly, if there's an error, the app will be harder to close (perhaps worse as it's less likely to show a Not Responding dialog). // Similarly, if there's an error, the app will be harder to close (perhaps worse as it's less likely to show a Not Responding dialog).
// And this also prevents it from closing with Ctrl+C in the terminal, which is arguably a feature. // And this also prevents it from closing with Ctrl+C in the terminal, which is arguably a feature.
mainWindow.webContents.send('close-window-prompt'); mainWindow.webContents.send('close-window-prompt');
event.preventDefault(); event.preventDefault();
}); });
// Open links without target=_blank externally. // Open links without target=_blank externally.
mainWindow.webContents.on('will-navigate', (e, url) => { mainWindow.webContents.on('will-navigate', (e, url) => {
// check that the URL is not part of the app // check that the URL is not part of the app
if(!url.includes("file://")){ if (!url.includes("file://")) {
e.preventDefault(); e.preventDefault();
shell.openExternal(url); shell.openExternal(url);
} }
@ -118,7 +118,7 @@ const createWindow = () => {
// Open links with target=_blank externally. // Open links with target=_blank externally.
mainWindow.webContents.setWindowOpenHandler(({ url }) => { mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// check that the URL is not part of the app // check that the URL is not part of the app
if(!url.includes("file://")){ if (!url.includes("file://")) {
shell.openExternal(url); shell.openExternal(url);
} }
return { action: "deny" }; return { action: "deny" };

View File

@ -18,8 +18,8 @@ if (/MSIE \d|Trident.*rv:/.test(navigator.userAgent)) {
document.write( document.write(
'<style>body { text-align: center; }</style>' + '<style>body { text-align: center; }</style>' +
'<div className="not-supported">' + '<div className="not-supported">' +
'<h1 className="not-supported-header">Internet Explorer is not supported!</h1>' + '<h1 className="not-supported-header">Internet Explorer is not supported!</h1>' +
'<p className="not-supported-details">Try Chrome, Firefox, or Edge.</p>' + '<p className="not-supported-details">Try Chrome, Firefox, or Edge.</p>' +
'</div>' '</div>'
); );
} }

View File

@ -11,31 +11,31 @@ extra_tools = [{
// @XXX: copy pasted all this brush caching/rendering code! // @XXX: copy pasted all this brush caching/rendering code!
// @TODO: DRY! // @TODO: DRY!
const csz = get_brush_canvas_size(brush_size, brush_shape); const csz = get_brush_canvas_size(brush_size, brush_shape);
if( if (
this.rendered_shape !== brush_shape || this.rendered_shape !== brush_shape ||
this.rendered_color !== stroke_color || this.rendered_color !== stroke_color ||
this.rendered_size !== brush_size this.rendered_size !== brush_size
){ ) {
brush_canvas.width = csz; brush_canvas.width = csz;
brush_canvas.height = csz; brush_canvas.height = csz;
// don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush // don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush
brush_ctx.fillStyle = brush_ctx.strokeStyle = stroke_color; brush_ctx.fillStyle = brush_ctx.strokeStyle = stroke_color;
render_brush(brush_ctx, brush_shape, brush_size); render_brush(brush_ctx, brush_shape, brush_size);
this.rendered_color = stroke_color; this.rendered_color = stroke_color;
this.rendered_size = brush_size; this.rendered_size = brush_size;
this.rendered_shape = brush_shape; this.rendered_shape = brush_shape;
} }
const draw_brush = (x, y) => { const draw_brush = (x, y) => {
ctx.drawImage(brush_canvas, Math.ceil(x-csz/2), Math.ceil(y-csz/2)); ctx.drawImage(brush_canvas, Math.ceil(x - csz / 2), Math.ceil(y - csz / 2));
}; };
const r = airbrush_size * 2; const r = airbrush_size * 2;
for(let i = 0; i < 6 + r/5; i++){ for (let i = 0; i < 6 + r / 5; i++) {
const rx = (Math.random()*2-1) * r; const rx = (Math.random() * 2 - 1) * r;
const ry = (Math.random()*2-1) * r; const ry = (Math.random() * 2 - 1) * r;
const d = rx*rx + ry*ry; const d = rx * rx + ry * ry;
if(d <= r * r){ if (d <= r * r) {
draw_brush(x + ~~rx, y + ~~ry); draw_brush(x + ~~rx, y + ~~ry);
} }
} }
@ -67,26 +67,26 @@ extra_tools = [{
// @XXX: copy pasted all this brush caching/rendering code! // @XXX: copy pasted all this brush caching/rendering code!
// @TODO: DRY! // @TODO: DRY!
const csz = get_brush_canvas_size(brush_size, brush_shape); const csz = get_brush_canvas_size(brush_size, brush_shape);
if( if (
this.rendered_shape !== brush_shape || this.rendered_shape !== brush_shape ||
this.rendered_color !== stroke_color || this.rendered_color !== stroke_color ||
this.rendered_size !== brush_size this.rendered_size !== brush_size
){ ) {
brush_canvas.width = csz; brush_canvas.width = csz;
brush_canvas.height = csz; brush_canvas.height = csz;
// don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush // don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush
brush_ctx.fillStyle = brush_ctx.strokeStyle = stroke_color; brush_ctx.fillStyle = brush_ctx.strokeStyle = stroke_color;
render_brush(brush_ctx, brush_shape, brush_size); render_brush(brush_ctx, brush_shape, brush_size);
this.rendered_color = stroke_color; this.rendered_color = stroke_color;
this.rendered_size = brush_size; this.rendered_size = brush_size;
this.rendered_shape = brush_shape; this.rendered_shape = brush_shape;
} }
const draw_brush = (x, y) => { const draw_brush = (x, y) => {
ctx.drawImage(brush_canvas, Math.ceil(x-csz/2), Math.ceil(y-csz/2)); ctx.drawImage(brush_canvas, Math.ceil(x - csz / 2), Math.ceil(y - csz / 2));
}; };
for(let i = 0; i < 60; i++){ for (let i = 0; i < 60; i++) {
const x_diff = x - this.position.x; const x_diff = x - this.position.x;
const y_diff = y - this.position.y; const y_diff = y - this.position.y;
const dist = Math.hypot(x_diff, y_diff); const dist = Math.hypot(x_diff, y_diff);
@ -107,7 +107,7 @@ extra_tools = [{
cursor: ["airbrush", [7, 22], "crosshair"], cursor: ["airbrush", [7, 22], "crosshair"],
continuous: "time", continuous: "time",
paint(ctx, x, y) { paint(ctx, x, y) {
}, },
$options: $choose_airbrush_size $options: $choose_airbrush_size
}]; }];

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ function show_help() {
root: "help", root: "help",
contentsFile: "help/mspaint.hhc", contentsFile: "help/mspaint.hhc",
}).$help_window; }).$help_window;
$help_window.on("close", ()=> { $help_window.on("close", () => {
$help_window = null; $help_window = null;
}); });
} }
@ -18,7 +18,7 @@ function show_help() {
// shared code with 98.js.org // shared code with 98.js.org
// (copy-pasted / manually synced for now) // (copy-pasted / manually synced for now)
function open_help_viewer(options){ function open_help_viewer(options) {
const $help_window = $Window({ const $help_window = $Window({
title: options.title || "Help Topics", title: options.title || "Help Topics",
icons: { icons: {
@ -34,19 +34,19 @@ function open_help_viewer(options){
const $main = $(E("div")).addClass("main"); const $main = $(E("div")).addClass("main");
const $toolbar = $(E("div")).addClass("toolbar"); const $toolbar = $(E("div")).addClass("toolbar");
const add_toolbar_button = (name, sprite_n, action_fn, enabled_fn)=> { const add_toolbar_button = (name, sprite_n, action_fn, enabled_fn) => {
const $button = $("<button class='lightweight'>") const $button = $("<button class='lightweight'>")
.append($("<span>").text(name)) .append($("<span>").text(name))
.appendTo($toolbar) .appendTo($toolbar)
.on("click", ()=> { .on("click", () => {
action_fn(); action_fn();
}); });
$("<div class='icon'/>") $("<div class='icon'/>")
.appendTo($button) .appendTo($button)
.css({ .css({
backgroundPosition: `${-sprite_n * 55}px 0px`, backgroundPosition: `${-sprite_n * 55}px 0px`,
}); });
const update_enabled = ()=> { const update_enabled = () => {
$button[0].disabled = enabled_fn && !enabled_fn(); $button[0].disabled = enabled_fn && !enabled_fn();
}; };
update_enabled(); update_enabled();
@ -54,12 +54,12 @@ function open_help_viewer(options){
$help_window.on("update-buttons", update_enabled); $help_window.on("update-buttons", update_enabled);
return $button; return $button;
}; };
const measure_sidebar_width = ()=> const measure_sidebar_width = () =>
$contents.outerWidth() + $contents.outerWidth() +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-left")) + parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-right")) + parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-right")) +
$resizer.outerWidth(); $resizer.outerWidth();
const $hide_button = add_toolbar_button("Hide", 0, ()=> { const $hide_button = add_toolbar_button("Hide", 0, () => {
const toggling_width = measure_sidebar_width(); const toggling_width = measure_sidebar_width();
$contents.hide(); $contents.hide();
$resizer.hide(); $resizer.hide();
@ -69,7 +69,7 @@ function open_help_viewer(options){
$help_window.css("left", $help_window.offset().left + toggling_width); $help_window.css("left", $help_window.offset().left + toggling_width);
$help_window.bringTitleBarInBounds(); $help_window.bringTitleBarInBounds();
}); });
const $show_button = add_toolbar_button("Show", 5, ()=> { const $show_button = add_toolbar_button("Show", 5, () => {
$contents.show(); $contents.show();
$resizer.show(); $resizer.show();
$show_button.hide(); $show_button.hide();
@ -86,31 +86,31 @@ function open_help_viewer(options){
} }
$help_window.css("max-width", ""); $help_window.css("max-width", "");
}).hide(); }).hide();
add_toolbar_button("Back", 1, ()=> { add_toolbar_button("Back", 1, () => {
$iframe[0].contentWindow.history.back(); $iframe[0].contentWindow.history.back();
ignore_one_load = true; ignore_one_load = true;
back_length -= 1; back_length -= 1;
forward_length += 1; forward_length += 1;
}, ()=> back_length > 0); }, () => back_length > 0);
add_toolbar_button("Forward", 2, ()=> { add_toolbar_button("Forward", 2, () => {
$iframe[0].contentWindow.history.forward(); $iframe[0].contentWindow.history.forward();
ignore_one_load = true; ignore_one_load = true;
forward_length -= 1; forward_length -= 1;
back_length += 1; back_length += 1;
}, ()=> forward_length > 0); }, () => forward_length > 0);
add_toolbar_button("Options", 3, ()=> {}, ()=> false); // @TODO: hotkey and underline on O add_toolbar_button("Options", 3, () => { }, () => false); // @TODO: hotkey and underline on O
add_toolbar_button("Web Help", 4, ()=> { add_toolbar_button("Web Help", 4, () => {
iframe.src = "help/online_support.htm"; iframe.src = "help/online_support.htm";
}); });
const $iframe = $Iframe({src: "help/default.html"}).addClass("inset-deep"); const $iframe = $Iframe({ src: "help/default.html" }).addClass("inset-deep");
const iframe = $iframe[0]; const iframe = $iframe[0];
iframe.$window = $help_window; // for focus handling integration iframe.$window = $help_window; // for focus handling integration
const $resizer = $(E("div")).addClass("resizer"); const $resizer = $(E("div")).addClass("resizer");
const $contents = $(E("ul")).addClass("contents inset-deep"); const $contents = $(E("ul")).addClass("contents inset-deep");
// @TODO: fix race conditions // @TODO: fix race conditions
$iframe.on("load", ()=> { $iframe.on("load", () => {
if (!ignore_one_load) { if (!ignore_one_load) {
back_length += 1; back_length += 1;
forward_length = 0; forward_length = 0;
@ -123,9 +123,9 @@ function open_help_viewer(options){
$main.append($contents, $resizer, $iframe); $main.append($contents, $resizer, $iframe);
$help_window.$content.append($toolbar, $main); $help_window.$content.append($toolbar, $main);
$help_window.css({width: 800, height: 600}); $help_window.css({ width: 800, height: 600 });
$iframe.attr({name: "help-frame"}); $iframe.attr({ name: "help-frame" });
$iframe.css({ $iframe.css({
backgroundColor: "white", backgroundColor: "white",
border: "", border: "",
@ -152,13 +152,13 @@ function open_help_viewer(options){
bottom: 0, bottom: 0,
zIndex: 1, zIndex: 1,
}); });
$resizer.on("pointerdown", (e)=> { $resizer.on("pointerdown", (e) => {
let pointermove, pointerup; let pointermove, pointerup;
const getPos = (e)=> const getPos = (e) =>
Math.min($help_window.width() - 100, Math.max(20, Math.min($help_window.width() - 100, Math.max(20,
e.clientX - $help_window.$content.offset().left e.clientX - $help_window.$content.offset().left
)); ));
$G.on("pointermove", pointermove = (e)=> { $G.on("pointermove", pointermove = (e) => {
$resizer.css({ $resizer.css({
position: "absolute", position: "absolute",
left: getPos(e) left: getPos(e)
@ -167,7 +167,7 @@ function open_help_viewer(options){
marginRight: resizer_width, marginRight: resizer_width,
}); });
}); });
$G.on("pointerup", pointerup = (e)=> { $G.on("pointerup", pointerup = (e) => {
$G.off("pointermove", pointermove); $G.off("pointermove", pointermove);
$G.off("pointerup", pointerup); $G.off("pointerup", pointerup);
$resizer.css({ $resizer.css({
@ -180,7 +180,7 @@ function open_help_viewer(options){
}); });
}); });
}); });
const parse_object_params = $object => { const parse_object_params = $object => {
// parse an $(<object>) to a plain object of key value pairs // parse an $(<object>) to a plain object of key value pairs
const object = {}; const object = {};
@ -189,9 +189,9 @@ function open_help_viewer(options){
} }
return object; return object;
}; };
let $last_expanded; let $last_expanded;
const $Item = text => { const $Item = text => {
const $item = $(E("div")).addClass("item").text(text.trim()); const $item = $(E("div")).addClass("item").text(text.trim());
$item.on("mousedown", () => { $item.on("mousedown", () => {
@ -200,8 +200,8 @@ function open_help_viewer(options){
}); });
$item.on("click", () => { $item.on("click", () => {
const $li = $item.parent(); const $li = $item.parent();
if($li.is(".folder")){ if ($li.is(".folder")) {
if($last_expanded){ if ($last_expanded) {
$last_expanded.not($li).removeClass("expanded"); $last_expanded.not($li).removeClass("expanded");
} }
$li.toggleClass("expanded"); $li.toggleClass("expanded");
@ -210,31 +210,31 @@ function open_help_viewer(options){
}); });
return $item; return $item;
}; };
const $default_item_li = $(E("li")).addClass("page"); const $default_item_li = $(E("li")).addClass("page");
$default_item_li.append($Item("Welcome to Help").on("click", ()=> { $default_item_li.append($Item("Welcome to Help").on("click", () => {
$iframe.attr({src: "help/default.html"}); $iframe.attr({ src: "help/default.html" });
})); }));
$contents.append($default_item_li); $contents.append($default_item_li);
function renderItem(source_li, $folder_items_ul) { function renderItem(source_li, $folder_items_ul) {
const object = parse_object_params($(source_li).children("object")); const object = parse_object_params($(source_li).children("object"));
if ($(source_li).find("li").length > 0){ if ($(source_li).find("li").length > 0) {
const $folder_li = $(E("li")).addClass("folder"); const $folder_li = $(E("li")).addClass("folder");
$folder_li.append($Item(object.Name)); $folder_li.append($Item(object.Name));
$contents.append($folder_li); $contents.append($folder_li);
const $folder_items_ul = $(E("ul")); const $folder_items_ul = $(E("ul"));
$folder_li.append($folder_items_ul); $folder_li.append($folder_items_ul);
$(source_li).children("ul").children().get().forEach((li)=> { $(source_li).children("ul").children().get().forEach((li) => {
renderItem(li, $folder_items_ul); renderItem(li, $folder_items_ul);
}); });
} else { } else {
const $item_li = $(E("li")).addClass("page"); const $item_li = $(E("li")).addClass("page");
$item_li.append($Item(object.Name).on("click", ()=> { $item_li.append($Item(object.Name).on("click", () => {
$iframe.attr({src: `${options.root}/${object.Local}`}); $iframe.attr({ src: `${options.root}/${object.Local}` });
})); }));
if ($folder_items_ul) { if ($folder_items_ul) {
$folder_items_ul.append($item_li); $folder_items_ul.append($item_li);
@ -244,15 +244,15 @@ function open_help_viewer(options){
} }
} }
fetch(options.contentsFile).then((response)=> { fetch(options.contentsFile).then((response) => {
response.text().then((hhc)=> { response.text().then((hhc) => {
$($.parseHTML(hhc)).filter("ul").children().get().forEach((li)=> { $($.parseHTML(hhc)).filter("ul").children().get().forEach((li) => {
renderItem(li, null); renderItem(li, null);
}); });
}, (error)=> { }, (error) => {
show_error_message(`${localize("Failed to launch help.")} Failed to read ${options.contentsFile}.`, error); show_error_message(`${localize("Failed to launch help.")} Failed to read ${options.contentsFile}.`, error);
}); });
}, (/* error */)=> { }, (/* error */) => {
// access to error message is not allowed either, basically // access to error message is not allowed either, basically
if (location.protocol === "file:") { if (location.protocol === "file:") {
showMessageBox({ showMessageBox({
@ -272,7 +272,7 @@ function open_help_viewer(options){
show_error_message(`${localize("Failed to launch help.")} ${localize("Access to %1 was denied.", options.contentsFile)}`); show_error_message(`${localize("Failed to launch help.")} ${localize("Access to %1 was denied.", options.contentsFile)}`);
} }
}); });
// @TODO: keyboard accessability // @TODO: keyboard accessability
// $help_window.on("keydown", (e)=> { // $help_window.on("keydown", (e)=> {
// switch(e.keyCode){ // switch(e.keyCode){
@ -290,64 +290,64 @@ function open_help_viewer(options){
var programs_being_loaded = 0; var programs_being_loaded = 0;
function $Iframe(options){ function $Iframe(options) {
var $iframe = $("<iframe allowfullscreen sandbox='allow-same-origin allow-scripts allow-forms allow-pointer-lock allow-modals allow-popups allow-downloads'>"); var $iframe = $("<iframe allowfullscreen sandbox='allow-same-origin allow-scripts allow-forms allow-pointer-lock allow-modals allow-popups allow-downloads'>");
var iframe = $iframe[0]; var iframe = $iframe[0];
var disable_delegate_pointerup = false; var disable_delegate_pointerup = false;
$iframe.focus_contents = function(){ $iframe.focus_contents = function () {
if (!iframe.contentWindow) { if (!iframe.contentWindow) {
return; return;
} }
if (iframe.contentDocument.hasFocus()) { if (iframe.contentDocument.hasFocus()) {
return; return;
} }
disable_delegate_pointerup = true; disable_delegate_pointerup = true;
iframe.contentWindow.focus(); iframe.contentWindow.focus();
setTimeout(function(){ setTimeout(function () {
iframe.contentWindow.focus(); iframe.contentWindow.focus();
disable_delegate_pointerup = false; disable_delegate_pointerup = false;
}); });
}; };
// Let the iframe to handle mouseup events outside itself // Let the iframe to handle mouseup events outside itself
var delegate_pointerup = function(){ var delegate_pointerup = function () {
if (disable_delegate_pointerup) { if (disable_delegate_pointerup) {
return; return;
} }
// This try-catch may only be needed for running Cypress tests. // This try-catch may only be needed for running Cypress tests.
try { try {
if(iframe.contentWindow && iframe.contentWindow.jQuery){ if (iframe.contentWindow && iframe.contentWindow.jQuery) {
iframe.contentWindow.jQuery("body").trigger("pointerup"); iframe.contentWindow.jQuery("body").trigger("pointerup");
} }
if(iframe.contentWindow){ if (iframe.contentWindow) {
const event = new iframe.contentWindow.MouseEvent("mouseup", {button: 0}); const event = new iframe.contentWindow.MouseEvent("mouseup", { button: 0 });
iframe.contentWindow.dispatchEvent(event); iframe.contentWindow.dispatchEvent(event);
const event2 = new iframe.contentWindow.MouseEvent("mouseup", {button: 2}); const event2 = new iframe.contentWindow.MouseEvent("mouseup", { button: 2 });
iframe.contentWindow.dispatchEvent(event2); iframe.contentWindow.dispatchEvent(event2);
} }
} catch(error) { } catch (error) {
console.log("Failed to access iframe to delegate pointerup; got", error); console.log("Failed to access iframe to delegate pointerup; got", error);
} }
}; };
$G.on("mouseup blur", delegate_pointerup); $G.on("mouseup blur", delegate_pointerup);
$iframe.destroy = ()=> { $iframe.destroy = () => {
$G.off("mouseup blur", delegate_pointerup); $G.off("mouseup blur", delegate_pointerup);
}; };
// @TODO: delegate pointermove events too? // @TODO: delegate pointermove events too?
$("body").addClass("loading-program"); $("body").addClass("loading-program");
programs_being_loaded += 1; programs_being_loaded += 1;
$iframe.on("load", function(){ $iframe.on("load", function () {
if(--programs_being_loaded <= 0){ if (--programs_being_loaded <= 0) {
$("body").removeClass("loading-program"); $("body").removeClass("loading-program");
} }
// This try-catch may only be needed for running Cypress tests. // This try-catch may only be needed for running Cypress tests.
try { try {
if (window.themeCSSProperties) { if (window.themeCSSProperties) {
@ -374,40 +374,40 @@ function $Iframe(options){
} }
var $contentWindow = $(iframe.contentWindow); var $contentWindow = $(iframe.contentWindow);
$contentWindow.on("pointerdown click", function(e){ $contentWindow.on("pointerdown click", function (e) {
iframe.$window && iframe.$window.focus(); iframe.$window && iframe.$window.focus();
// from close_menus in $MenuBar // from close_menus in $MenuBar
$(".menu-button").trigger("release"); $(".menu-button").trigger("release");
// Close any rogue floating submenus // Close any rogue floating submenus
$(".menu-popup").hide(); $(".menu-popup").hide();
}); });
// We want to disable pointer events for other iframes, but not this one // We want to disable pointer events for other iframes, but not this one
$contentWindow.on("pointerdown", function(e){ $contentWindow.on("pointerdown", function (e) {
$iframe.css("pointer-events", "all"); $iframe.css("pointer-events", "all");
$("body").addClass("dragging"); $("body").addClass("dragging");
}); });
$contentWindow.on("pointerup", function(e){ $contentWindow.on("pointerup", function (e) {
$("body").removeClass("dragging"); $("body").removeClass("dragging");
$iframe.css("pointer-events", ""); $iframe.css("pointer-events", "");
}); });
// $("iframe").css("pointer-events", ""); is called elsewhere. // $("iframe").css("pointer-events", ""); is called elsewhere.
// Otherwise iframes would get stuck in this interaction mode // Otherwise iframes would get stuck in this interaction mode
iframe.contentWindow.close = function(){ iframe.contentWindow.close = function () {
iframe.$window && iframe.$window.close(); iframe.$window && iframe.$window.close();
}; };
// @TODO: hook into saveAs (a la FileSaver.js) and another function for opening files // @TODO: hook into saveAs (a la FileSaver.js) and another function for opening files
// iframe.contentWindow.saveAs = function(){ // iframe.contentWindow.saveAs = function(){
// saveAsDialog(); // saveAsDialog();
// }; // };
} catch(error) { } catch (error) {
console.log("Failed to reach into iframe; got", error); console.log("Failed to reach into iframe; got", error);
} }
}); });
if (options.src) { if (options.src) {
$iframe.attr({src: options.src}); $iframe.attr({ src: options.src });
} }
$iframe.css({ $iframe.css({
minWidth: 0, minWidth: 0,
@ -420,9 +420,9 @@ function $Iframe(options){
} }
// function $IframeWindow(options){ // function $IframeWindow(options){
// var $win = new $Window(options); // var $win = new $Window(options);
// var $iframe = $win.$iframe = $Iframe({src: options.src}); // var $iframe = $win.$iframe = $Iframe({src: options.src});
// $win.$content.append($iframe); // $win.$content.append($iframe);
// var iframe = $win.iframe = $iframe[0]; // var iframe = $win.iframe = $iframe[0];
@ -441,7 +441,7 @@ function $Iframe(options){
// $win.focus(); // $win.focus();
// // $iframe.focus_contents(); // // $iframe.focus_contents();
// }); // });
// $win.setInnerDimensions = ({width, height})=> { // $win.setInnerDimensions = ({width, height})=> {
// const width_from_frame = $win.width() - $win.$content.width(); // const width_from_frame = $win.width() - $win.$content.width();
// const height_from_frame = $win.height() - $win.$content.height(); // const height_from_frame = $win.height() - $win.$content.height();
@ -458,25 +458,25 @@ function $Iframe(options){
// display: "flex", // display: "flex",
// flexDirection: "column", // flexDirection: "column",
// }); // });
// // @TODO: cascade windows // // @TODO: cascade windows
// $win.center(); // $win.center();
// $win.hide(); // $win.hide();
// return $win; // return $win;
// } // }
// Fix dragging things (i.e. windows) over iframes (i.e. other windows) // Fix dragging things (i.e. windows) over iframes (i.e. other windows)
// (when combined with a bit of css, .dragging iframe { pointer-events: none; }) // (when combined with a bit of css, .dragging iframe { pointer-events: none; })
// (and a similar thing in $IframeWindow) // (and a similar thing in $IframeWindow)
$(window).on("pointerdown", function(e){ $(window).on("pointerdown", function (e) {
//console.log(e.type); //console.log(e.type);
$("body").addClass("dragging"); $("body").addClass("dragging");
}); });
$(window).on("pointerup dragend blur", function(e){ $(window).on("pointerup dragend blur", function (e) {
//console.log(e.type); //console.log(e.type);
if(e.type === "blur"){ if (e.type === "blur") {
if(document.activeElement.tagName.match(/iframe/i)){ if (document.activeElement.tagName.match(/iframe/i)) {
return; return;
} }
} }

View File

@ -1,35 +1,35 @@
const TAU = //////|////// const TAU = //////|//////
///// | ///// ///// | /////
/// tau /// /// tau ///
/// ...--> | <--... /// /// ...--> | <--... ///
/// -' one | turn '- /// /// -' one | turn '- ///
// .' | '. // // .' | '. //
// / | \ // // / | \ //
// | | <-.. | // // | | <-.. | //
// | .->| \ | // // | .->| \ | //
// | / | | | // // | / | | | //
- - - - - - Math.PI + Math.PI - - - - - 0; - - - - - - Math.PI + Math.PI - - - - - 0;
// | \ | | | // // | \ | | | //
// | '->| / | // // | '->| / | //
// | | <-'' | // // | | <-'' | //
// \ | / // // \ | / //
// '. | .' // // '. | .' //
/// -. | .- /// /// -. | .- ///
/// '''----|----''' /// /// '''----|----''' ///
/// | /// /// | ///
////// | ///// ////// | /////
//////|////// C/r; //////|////// C/r;
const is_pride_month = new Date().getMonth() === 5; // June (0-based, 0 is January) const is_pride_month = new Date().getMonth() === 5; // June (0-based, 0 is January)
const $G = $(window); const $G = $(window);
function make_css_cursor(name, coords, fallback){ function make_css_cursor(name, coords, fallback) {
return `url(images/cursors/${name}.png) ${coords.join(" ")}, ${fallback}`; return `url(images/cursors/${name}.png) ${coords.join(" ")}, ${fallback}`;
} }
function E(t){ function E(t) {
return document.createElement(t); return document.createElement(t);
} }
@ -39,11 +39,11 @@ N milliseconds. If `immediate` is passed, trigger the function on the
leading edge, instead of the trailing. */ leading edge, instead of the trailing. */
function debounce(func, wait_ms, immediate) { function debounce(func, wait_ms, immediate) {
let timeout; let timeout;
const debounced_func = function() { const debounced_func = function () {
const context = this; const context = this;
const args = arguments; const args = arguments;
const later = ()=> { const later = () => {
timeout = null; timeout = null;
if (!immediate) { if (!immediate) {
func.apply(context, args); func.apply(context, args);
@ -66,17 +66,17 @@ function debounce(func, wait_ms, immediate) {
return debounced_func; return debounced_func;
} }
function memoize_synchronous_function(func, max_entries=50000) { function memoize_synchronous_function(func, max_entries = 50000) {
const cache = {}; const cache = {};
const keys = []; const keys = [];
const memoized_func = (...args)=> { const memoized_func = (...args) => {
if (args.some((arg)=> arg instanceof CanvasPattern)) { if (args.some((arg) => arg instanceof CanvasPattern)) {
return func.apply(null, args); return func.apply(null, args);
} }
const key = JSON.stringify(args); const key = JSON.stringify(args);
if (cache[key]){ if (cache[key]) {
return cache[key]; return cache[key];
} else{ } else {
const val = func.apply(null, args); const val = func.apply(null, args);
cache[key] = val; cache[key] = val;
keys.push(key); keys.push(key);
@ -84,10 +84,10 @@ function memoize_synchronous_function(func, max_entries=50000) {
const oldest_key = keys.shift(); const oldest_key = keys.shift();
delete cache[oldest_key]; delete cache[oldest_key];
} }
return val; return val;
} }
} }
memoized_func.clear_memo_cache = ()=> { memoized_func.clear_memo_cache = () => {
for (const key of keys) { for (const key of keys) {
delete cache[key]; delete cache[key];
} }
@ -96,14 +96,14 @@ function memoize_synchronous_function(func, max_entries=50000) {
return memoized_func; return memoized_func;
} }
window.get_rgba_from_color = memoize_synchronous_function((color)=> { window.get_rgba_from_color = memoize_synchronous_function((color) => {
const single_pixel_canvas = make_canvas(1, 1); const single_pixel_canvas = make_canvas(1, 1);
single_pixel_canvas.ctx.fillStyle = color; single_pixel_canvas.ctx.fillStyle = color;
single_pixel_canvas.ctx.fillRect(0, 0, 1, 1); single_pixel_canvas.ctx.fillRect(0, 0, 1, 1);
const image_data = single_pixel_canvas.ctx.getImageData(0, 0, 1, 1); const image_data = single_pixel_canvas.ctx.getImageData(0, 0, 1, 1);
// We could just return image_data.data, but let's return an Array instead // We could just return image_data.data, but let's return an Array instead
// I'm not totally sure image_data.data wouldn't keep the ImageData object around in memory // I'm not totally sure image_data.data wouldn't keep the ImageData object around in memory
return Array.from(image_data.data); return Array.from(image_data.data);
@ -130,15 +130,15 @@ function image_data_match(a, b, threshold) {
return true; return true;
} }
function make_canvas(width, height){ function make_canvas(width, height) {
const image = width; const image = width;
const new_canvas = E("canvas"); const new_canvas = E("canvas");
const new_ctx = new_canvas.getContext("2d"); const new_ctx = new_canvas.getContext("2d");
new_canvas.ctx = new_ctx; new_canvas.ctx = new_ctx;
new_ctx.disable_image_smoothing = ()=> { new_ctx.disable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = false; new_ctx.imageSmoothingEnabled = false;
// condition is to avoid a deprecation warning in Firefox // condition is to avoid a deprecation warning in Firefox
if (new_ctx.imageSmoothingEnabled !== false) { if (new_ctx.imageSmoothingEnabled !== false) {
@ -147,7 +147,7 @@ function make_canvas(width, height){
new_ctx.msImageSmoothingEnabled = false; new_ctx.msImageSmoothingEnabled = false;
} }
}; };
new_ctx.enable_image_smoothing = ()=> { new_ctx.enable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = true; new_ctx.imageSmoothingEnabled = true;
if (new_ctx.imageSmoothingEnabled !== true) { if (new_ctx.imageSmoothingEnabled !== true) {
new_ctx.mozImageSmoothingEnabled = true; new_ctx.mozImageSmoothingEnabled = true;
@ -155,36 +155,36 @@ function make_canvas(width, height){
new_ctx.msImageSmoothingEnabled = true; new_ctx.msImageSmoothingEnabled = true;
} }
}; };
// @TODO: simplify the abstraction by defining setters for width/height // @TODO: simplify the abstraction by defining setters for width/height
// that reset the image smoothing to disabled // that reset the image smoothing to disabled
// and make image smoothing a parameter to make_canvas // and make image smoothing a parameter to make_canvas
new_ctx.copy = image => { new_ctx.copy = image => {
new_canvas.width = image.naturalWidth || image.width; new_canvas.width = image.naturalWidth || image.width;
new_canvas.height = image.naturalHeight || image.height; new_canvas.height = image.naturalHeight || image.height;
// setting width/height resets image smoothing (along with everything) // setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing(); new_ctx.disable_image_smoothing();
if (image instanceof ImageData) { if (image instanceof ImageData) {
new_ctx.putImageData(image, 0, 0); new_ctx.putImageData(image, 0, 0);
} else { } else {
new_ctx.drawImage(image, 0, 0); new_ctx.drawImage(image, 0, 0);
} }
}; };
if(width && height){ if (width && height) {
// make_canvas(width, height) // make_canvas(width, height)
new_canvas.width = width; new_canvas.width = width;
new_canvas.height = height; new_canvas.height = height;
// setting width/height resets image smoothing (along with everything) // setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing(); new_ctx.disable_image_smoothing();
}else if(image){ } else if (image) {
// make_canvas(image) // make_canvas(image)
new_ctx.copy(image); new_ctx.copy(image);
} }
return new_canvas; return new_canvas;
} }
@ -200,11 +200,11 @@ function get_icon_for_tool(tool) {
// not to be confused with load_image_from_uri // not to be confused with load_image_from_uri
function load_image_simple(src) { function load_image_simple(src) {
return new Promise((resolve, reject)=> { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onload = ()=> { resolve(img); }; img.onload = () => { resolve(img); };
img.onerror = ()=> { reject(new Error(`failed to load image from ${src}`)); }; img.onerror = () => { reject(new Error(`failed to load image from ${src}`)); };
img.src = src; img.src = src;
}); });
@ -216,16 +216,16 @@ function get_icon_for_tools(tools) {
} }
const icon_canvas = make_canvas(16, 16); const icon_canvas = make_canvas(16, 16);
Promise.all(tools.map((tool)=> load_image_simple(`help/${tool.help_icon}`))) Promise.all(tools.map((tool) => load_image_simple(`help/${tool.help_icon}`)))
.then((icons)=> { .then((icons) => {
icons.forEach((icon, i)=> { icons.forEach((icon, i) => {
const w = icon_canvas.width / icons.length; const w = icon_canvas.width / icons.length;
const x = i * w; const x = i * w;
const h = icon_canvas.height; const h = icon_canvas.height;
const y = 0; const y = 0;
icon_canvas.ctx.drawImage(icon, x, y, w, h, x, y, w, h); icon_canvas.ctx.drawImage(icon, x, y, w, h, x, y, w, h);
}); });
}) })
return icon_canvas; return icon_canvas;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
let $imgur_window; let $imgur_window;
function show_imgur_uploader(blob){ function show_imgur_uploader(blob) {
if($imgur_window){ if ($imgur_window) {
$imgur_window.close(); $imgur_window.close();
} }
$imgur_window = $DialogWindow().title("Upload To Imgur").addClass("horizontal-buttons"); $imgur_window = $DialogWindow().title("Upload To Imgur").addClass("horizontal-buttons");
const $preview_image_area = $(E("div")).appendTo($imgur_window.$main).addClass("inset-deep"); const $preview_image_area = $(E("div")).appendTo($imgur_window.$main).addClass("inset-deep");
const $imgur_url_area = $(E("div")).appendTo($imgur_window.$main); const $imgur_url_area = $(E("div")).appendTo($imgur_window.$main);
const $imgur_status = $(E("div")).appendTo($imgur_window.$main); const $imgur_status = $(E("div")).appendTo($imgur_window.$main);
// @TODO: maybe make this preview small but zoomable to full size? // @TODO: maybe make this preview small but zoomable to full size?
// (starting small (max-width: 100%) and toggling to either scrollable or fullscreen) // (starting small (max-width: 100%) and toggling to either scrollable or fullscreen)
// it should be clear that it's not going to upload a downsized version of your image // it should be clear that it's not going to upload a downsized version of your image
@ -17,7 +17,7 @@ function show_imgur_uploader(blob){
display: "block", // prevent margin below due to inline display (vertical-align can also be used) display: "block", // prevent margin below due to inline display (vertical-align can also be used)
}); });
const blob_url = URL.createObjectURL(blob); const blob_url = URL.createObjectURL(blob);
$preview_image.attr({src: blob_url}); $preview_image.attr({ src: blob_url });
// $preview_image.css({maxWidth: "100%", maxHeight: "400px"}); // $preview_image.css({maxWidth: "100%", maxHeight: "400px"});
$preview_image_area.css({ $preview_image_area.css({
maxWidth: "90vw", maxWidth: "90vw",
@ -26,7 +26,7 @@ function show_imgur_uploader(blob){
marginBottom: "0.5em", marginBottom: "0.5em",
}); });
$preview_image.on("load", () => { $preview_image.on("load", () => {
$imgur_window.css({width: "auto"}); $imgur_window.css({ width: "auto" });
$imgur_window.center(); $imgur_window.center();
}); });
$imgur_window.on("close", () => { $imgur_window.on("close", () => {
@ -53,7 +53,7 @@ function show_imgur_uploader(blob){
const parseImgurResponseJSON = responseJSON => { const parseImgurResponseJSON = responseJSON => {
try { try {
return JSON.parse(responseJSON); return JSON.parse(responseJSON);
} catch(error) { } catch (error) {
$imgur_status.text("Received an invalid JSON response from Imgur: "); $imgur_status.text("Received an invalid JSON response from Imgur: ");
// .append($(E("pre")).text(responseJSON)); // .append($(E("pre")).text(responseJSON));
@ -63,26 +63,26 @@ function show_imgur_uploader(blob){
// @TODO: DRY, including with show_error_message // @TODO: DRY, including with show_error_message
$(E("pre")) $(E("pre"))
.appendTo($imgur_status) .appendTo($imgur_status)
.text(responseJSON) .text(responseJSON)
.css({ .css({
background: "white", background: "white",
color: "#333", color: "#333",
fontFamily: "monospace", fontFamily: "monospace",
width: "500px", width: "500px",
overflow: "auto", overflow: "auto",
}); });
$(E("pre")) $(E("pre"))
.appendTo($imgur_status) .appendTo($imgur_status)
.text(error.toString()) .text(error.toString())
.css({ .css({
background: "white", background: "white",
color: "#333", color: "#333",
fontFamily: "monospace", fontFamily: "monospace",
width: "500px", width: "500px",
overflow: "auto", overflow: "auto",
}); });
$imgur_window.css({width: "auto"}); $imgur_window.css({ width: "auto" });
$imgur_window.center(); $imgur_window.center();
} }
}; };
@ -91,9 +91,9 @@ function show_imgur_uploader(blob){
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
if(req.upload){ if (req.upload) {
req.upload.addEventListener('progress', event => { req.upload.addEventListener('progress', event => {
if(event.lengthComputable){ if (event.lengthComputable) {
const progress_value = event.loaded / event.total; const progress_value = event.loaded / event.total;
const percentage_text = `${Math.floor(progress_value * 100)}%`; const percentage_text = `${Math.floor(progress_value * 100)}%`;
$progress.val(progress_value); $progress.val(progress_value);
@ -102,14 +102,14 @@ function show_imgur_uploader(blob){
}, false); }, false);
} }
req.addEventListener("readystatechange", () => { req.addEventListener("readystatechange", () => {
if(req.readyState == 4 && req.status == 200){ if (req.readyState == 4 && req.status == 200) {
$progress.add($progress_percent).remove(); $progress.add($progress_percent).remove();
const response = parseImgurResponseJSON(req.responseText); const response = parseImgurResponseJSON(req.responseText);
if(!response) return; if (!response) return;
if(!response.success){ if (!response.success) {
$imgur_status.text("Failed to upload image :("); $imgur_status.text("Failed to upload image :(");
return; return;
} }
@ -117,7 +117,7 @@ function show_imgur_uploader(blob){
$imgur_status.text(""); $imgur_status.text("");
const $imgur_url = $(E("a")).attr({id: "imgur-url", target: "_blank"}); const $imgur_url = $(E("a")).attr({ id: "imgur-url", target: "_blank" });
$imgur_url.text(url); $imgur_url.text(url);
$imgur_url.attr('href', url); $imgur_url.attr('href', url);
@ -126,27 +126,27 @@ function show_imgur_uploader(blob){
).append($imgur_url); ).append($imgur_url);
// @TODO: a button to copy the URL to the clipboard // @TODO: a button to copy the URL to the clipboard
// (also maybe put the URL in a readonly input) // (also maybe put the URL in a readonly input)
let $ok_button; let $ok_button;
const $delete_button = $imgur_window.$Button("Delete", () => { const $delete_button = $imgur_window.$Button("Delete", () => {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
$delete_button[0].disabled = true; $delete_button[0].disabled = true;
req.addEventListener("readystatechange", () => { req.addEventListener("readystatechange", () => {
if(req.readyState == 4 && req.status == 200){ if (req.readyState == 4 && req.status == 200) {
$delete_button.remove(); $delete_button.remove();
$ok_button.focus(); $ok_button.focus();
const response = parseImgurResponseJSON(req.responseText); const response = parseImgurResponseJSON(req.responseText);
if(!response) return; if (!response) return;
if(response.success){ if (response.success) {
$imgur_url_area.remove(); $imgur_url_area.remove();
$imgur_status.text("Deleted successfully"); $imgur_status.text("Deleted successfully");
}else{ } else {
$imgur_status.text("Failed to delete image :("); $imgur_status.text("Failed to delete image :(");
} }
}else if(req.readyState == 4){ } else if (req.readyState == 4) {
$imgur_status.text("Error deleting image :("); $imgur_status.text("Error deleting image :(");
$delete_button[0].disabled = false; $delete_button[0].disabled = false;
$delete_button.focus(); $delete_button.focus();
@ -154,7 +154,7 @@ function show_imgur_uploader(blob){
}); });
req.open("DELETE", `https://api.imgur.com/3/image/${response.data.deletehash}`, true); req.open("DELETE", `https://api.imgur.com/3/image/${response.data.deletehash}`, true);
req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1"); req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1");
req.setRequestHeader("Accept", "application/json"); req.setRequestHeader("Accept", "application/json");
req.send(null); req.send(null);
@ -164,14 +164,14 @@ function show_imgur_uploader(blob){
$ok_button = $imgur_window.$Button(localize("OK"), () => { $ok_button = $imgur_window.$Button(localize("OK"), () => {
$imgur_window.close(); $imgur_window.close();
}).focus(); }).focus();
}else if(req.readyState == 4){ } else if (req.readyState == 4) {
$progress.add($progress_percent).remove(); $progress.add($progress_percent).remove();
$imgur_status.text("Error uploading image :("); $imgur_status.text("Error uploading image :(");
} }
}); });
req.open("POST", "https://api.imgur.com/3/image", true); req.open("POST", "https://api.imgur.com/3/image", true);
const form_data = new FormData(); const form_data = new FormData();
form_data.append("image", blob); form_data.append("image", blob);

View File

@ -3,12 +3,12 @@ let $storage_manager;
let $quota_exceeded_window; let $quota_exceeded_window;
let ignoring_quota_exceeded = false; let ignoring_quota_exceeded = false;
async function storage_quota_exceeded(){ async function storage_quota_exceeded() {
if($quota_exceeded_window){ if ($quota_exceeded_window) {
$quota_exceeded_window.close(); $quota_exceeded_window.close();
$quota_exceeded_window = null; $quota_exceeded_window = null;
} }
if(ignoring_quota_exceeded){ if (ignoring_quota_exceeded) {
return; return;
} }
const { promise, $window } = showMessageBox({ const { promise, $window } = showMessageBox({
@ -34,8 +34,8 @@ async function storage_quota_exceeded(){
} }
} }
function manage_storage(){ function manage_storage() {
if($storage_manager){ if ($storage_manager) {
$storage_manager.close(); $storage_manager.close();
} }
$storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager squish"); $storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager squish");
@ -47,29 +47,29 @@ function manage_storage(){
$storage_manager.$Button("Close", () => { $storage_manager.$Button("Close", () => {
$storage_manager.close(); $storage_manager.close();
}); });
const addRow = (k, imgSrc) => { const addRow = (k, imgSrc) => {
const $tr = $(E("tr")).appendTo($table); const $tr = $(E("tr")).appendTo($table);
const $img = $(E("img")).attr({ src: imgSrc }).addClass("thumbnail-img"); const $img = $(E("img")).attr({ src: imgSrc }).addClass("thumbnail-img");
const $remove = $(E("button")).text("Remove").addClass("remove-button"); const $remove = $(E("button")).text("Remove").addClass("remove-button");
const href = `#${k.replace("image#", "local:")}`; const href = `#${k.replace("image#", "local:")}`;
const $open_link = $(E("a")).attr({href, target: "_blank"}).text(localize("Open")); const $open_link = $(E("a")).attr({ href, target: "_blank" }).text(localize("Open"));
const $thumbnail_open_link = $(E("a")).attr({href, target: "_blank"}).addClass("thumbnail-container"); const $thumbnail_open_link = $(E("a")).attr({ href, target: "_blank" }).addClass("thumbnail-container");
$thumbnail_open_link.append($img); $thumbnail_open_link.append($img);
$(E("td")).append($thumbnail_open_link).appendTo($tr); $(E("td")).append($thumbnail_open_link).appendTo($tr);
$(E("td")).append($open_link).appendTo($tr); $(E("td")).append($open_link).appendTo($tr);
$(E("td")).append($remove).appendTo($tr); $(E("td")).append($remove).appendTo($tr);
$remove.on("click", () => { $remove.on("click", () => {
localStorage.removeItem(k); localStorage.removeItem(k);
$tr.remove(); $tr.remove();
if($table.find("tr").length == 0){ if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>"); $message.html("<p>All clear!</p>");
} }
}); });
}; };
let localStorageAvailable = false; let localStorageAvailable = false;
try { try {
if (localStorage.length > 0) { if (localStorage.length > 0) {
@ -82,19 +82,19 @@ function manage_storage(){
localStorageAvailable = localStorage._available; localStorageAvailable = localStorage._available;
delete localStorage._available; delete localStorage._available;
} }
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) { }
if (localStorageAvailable) { if (localStorageAvailable) {
for(const k in localStorage){ for (const k in localStorage) {
if(k.match(/^image#/)){ if (k.match(/^image#/)) {
let v = localStorage[k]; let v = localStorage[k];
try { try {
if (v[0] === '"') { if (v[0] === '"') {
v = JSON.parse(v); v = JSON.parse(v);
} }
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) { }
addRow(k, v); addRow(k, v);
} }
} }
@ -104,7 +104,7 @@ function manage_storage(){
// @TODO: DRY with similar message // @TODO: DRY with similar message
// @TODO: instructions for your browser; it's called Cookies in chrome/chromium at least, and "storage" gives NO results // @TODO: instructions for your browser; it's called Cookies in chrome/chromium at least, and "storage" gives NO results
$message.html("<p>Please enable local storage in your browser's settings for local backup. It may be called Cookies, Storage, or Site Data.</p>"); $message.html("<p>Please enable local storage in your browser's settings for local backup. It may be called Cookies, Storage, or Site Data.</p>");
} else if($table.find("tr").length == 0) { } else if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>"); $message.html("<p>All clear!</p>");
} }

View File

@ -1,4 +1,4 @@
(()=> { (() => {
const looksLikeChrome = !!(window.chrome && (window.chrome.loadTimes || window.chrome.csi)); const looksLikeChrome = !!(window.chrome && (window.chrome.loadTimes || window.chrome.csi));
// NOTE: Microsoft Edge includes window.chrome.app // NOTE: Microsoft Edge includes window.chrome.app
@ -12,7 +12,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"new", "new file", "new document", "create new document", "create a new document", "start new document", "start a new document", "new", "new file", "new document", "create new document", "create a new document", "start new document", "start a new document",
], ],
action: ()=> { file_new(); }, action: () => { file_new(); },
description: localize("Creates a new document."), description: localize("Creates a new document."),
}, },
{ {
@ -24,7 +24,7 @@ window.menus = {
"show file picker", "show file chooser", "show file browser", "show finder", "show file picker", "show file chooser", "show file browser", "show finder",
"browser for file", "browse for a file", "browse for an image", "browse for an image file", "browser for file", "browse for a file", "browse for an image", "browse for an image file",
], ],
action: ()=> { file_open(); }, action: () => { file_open(); },
description: localize("Opens an existing document."), description: localize("Opens an existing document."),
}, },
{ {
@ -38,7 +38,7 @@ window.menus = {
"download", "download document", "download file", "download image", "download picture", "download image file", "download", "download document", "download file", "download image", "download picture", "download image file",
"download the document", "download the file", "download the image", "download the image file", "download the document", "download the file", "download the image", "download the image file",
], ],
action: ()=> { file_save(); }, action: () => { file_save(); },
description: localize("Saves the active document."), description: localize("Saves the active document."),
}, },
{ {
@ -57,7 +57,7 @@ window.menus = {
"save file as a copy", "save file copy", "save file as copy", "save file under a new name", "save file with a new name", "save file as a copy", "save file copy", "save file as copy", "save file under a new name", "save file with a new name",
"save image file as a copy", "save image file copy", "save image file as copy", "save image file under a new name", "save image file with a new name", "save image file as a copy", "save image file copy", "save image file as copy", "save image file under a new name", "save image file with a new name",
], ],
action: ()=> { file_save_as(); }, action: () => { file_save_as(); },
description: localize("Saves the active document with a new name."), description: localize("Saves the active document with a new name."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -98,7 +98,7 @@ window.menus = {
"load picture from address", "load picture from address",
"load picture from web address", "load picture from web address",
], ],
action: ()=> { file_load_from_url(); }, action: () => { file_load_from_url(); },
description: localize("Opens an image from the web."), description: localize("Opens an image from the web."),
}, },
{ {
@ -106,12 +106,12 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"upload to imgur", "upload image to imgur", "upload picture to imgur", "upload to imgur", "upload image to imgur", "upload picture to imgur",
], ],
action: ()=> { action: () => {
// include the selection in the saved image // include the selection in the saved image
deselect(); deselect();
main_canvas.toBlob((blob)=> { main_canvas.toBlob((blob) => {
sanity_check_blob(blob, ()=> { sanity_check_blob(blob, () => {
show_imgur_uploader(blob); show_imgur_uploader(blob);
}); });
}); });
@ -126,7 +126,7 @@ window.menus = {
"show autosaves", "show saves", "show saved documents", "show saved files", "show saved pictures", "show saved images", "show local storage", "show autosaves", "show saves", "show saved documents", "show saved files", "show saved pictures", "show saved images", "show local storage",
"autosaves", "autosave", "saved documents", "saved files", "saved pictures", "saved images", "local storage", "autosaves", "autosave", "saved documents", "saved files", "saved pictures", "saved images", "local storage",
], ],
action: ()=> { manage_storage(); }, action: () => { manage_storage(); },
description: localize("Manages storage of previously created or opened pictures."), description: localize("Manages storage of previously created or opened pictures."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -135,7 +135,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"preview print", "print preview", "show print preview", "show preview of print", "preview print", "print preview", "show print preview", "show preview of print",
], ],
action: ()=> { action: () => {
print(); print();
}, },
description: localize("Prints the active document and sets printing options."), description: localize("Prints the active document and sets printing options."),
@ -147,7 +147,7 @@ window.menus = {
"setup page for print", "setup page for printing", "set-up page for print", "set-up page for printing", "set up page for print", "set up page for printing", "setup page for print", "setup page for printing", "set-up page for print", "set-up page for printing", "set up page for print", "set up page for printing",
"page setup", "printing setup", "page set-up", "printing set-up", "page set up", "printing set up", "page setup", "printing setup", "page set-up", "printing set-up", "page set up", "printing set up",
], ],
action: ()=> { action: () => {
print(); print();
}, },
description: localize("Prints the active document and sets printing options."), description: localize("Prints the active document and sets printing options."),
@ -159,15 +159,15 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"print", "send to printer", "show print dialog", "print", "send to printer", "show print dialog",
"print page", "print image", "print picture", "print drawing", "print page", "print image", "print picture", "print drawing",
"print out page", "print out image", "print out picture", "print out drawing", "print out page", "print out image", "print out picture", "print out drawing",
"print out the page", "print out the image", "print out the picture", "print out the drawing", "print out the page", "print out the image", "print out the picture", "print out the drawing",
"send page to printer", "send image to printer", "send picture to printer", "send drawing to printer", "send page to printer", "send image to printer", "send picture to printer", "send drawing to printer",
"send page to the printer", "send image to the printer", "send picture to the printer", "send drawing to the printer", "send page to the printer", "send image to the printer", "send picture to the printer", "send drawing to the printer",
"send the page to the printer", "send the image to the printer", "send the picture to the printer", "send the drawing to the printer", "send the page to the printer", "send the image to the printer", "send the picture to the printer", "send the drawing to the printer",
"send the page to printer", "send the image to printer", "send the picture to printer", "send the drawing to printer", "send the page to printer", "send the image to printer", "send the picture to printer", "send the drawing to printer",
], ],
action: ()=> { action: () => {
print(); print();
}, },
description: localize("Prints the active document and sets printing options."), description: localize("Prints the active document and sets printing options."),
@ -178,24 +178,24 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"set as wallpaper", "set as wallpaper",
"set as wallpaper tiled", "set as wallpaper tiled",
"set image as wallpaper tiled", "set picture as wallpaper tiled", "set drawing as wallpaper tiled", "set image as wallpaper tiled", "set picture as wallpaper tiled", "set drawing as wallpaper tiled",
"use as wallpaper tiled", "use as wallpaper tiled",
"use image as wallpaper tiled", "use picture as wallpaper tiled", "use drawing as wallpaper tiled", "use image as wallpaper tiled", "use picture as wallpaper tiled", "use drawing as wallpaper tiled",
"tile image as wallpaper", "tile picture as wallpaper", "tile drawing as wallpaper", "tile image as wallpaper", "tile picture as wallpaper", "tile drawing as wallpaper",
], ],
action: ()=> { systemHooks.setWallpaperTiled(main_canvas); }, action: () => { systemHooks.setWallpaperTiled(main_canvas); },
description: localize("Tiles this bitmap as the desktop background."), description: localize("Tiles this bitmap as the desktop background."),
}, },
{ {
item: localize("Set As Wallpaper (&Centered)"), // in mspaint it's Wa&llpaper item: localize("Set As Wallpaper (&Centered)"), // in mspaint it's Wa&llpaper
speech_recognition: [ speech_recognition: [
"set as wallpaper centered", "set as wallpaper centered",
"set image as wallpaper centered", "set picture as wallpaper centered", "set drawing as wallpaper centered", "set image as wallpaper centered", "set picture as wallpaper centered", "set drawing as wallpaper centered",
"use as wallpaper centered", "use as wallpaper centered",
"use image as wallpaper centered", "use picture as wallpaper centered", "use drawing as wallpaper centered", "use image as wallpaper centered", "use picture as wallpaper centered", "use drawing as wallpaper centered",
"center image as wallpaper", "center picture as wallpaper", "center drawing as wallpaper", "center image as wallpaper", "center picture as wallpaper", "center drawing as wallpaper",
], ],
action: ()=> { systemHooks.setWallpaperCentered(main_canvas); }, action: () => { systemHooks.setWallpaperCentered(main_canvas); },
description: localize("Centers this bitmap as the desktop background."), description: localize("Centers this bitmap as the desktop background."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -240,7 +240,7 @@ window.menus = {
"undo", "undo that", "undo", "undo that",
], ],
enabled: () => undos.length >= 1, enabled: () => undos.length >= 1,
action: ()=> { undo(); }, action: () => { undo(); },
description: localize("Undoes the last action."), description: localize("Undoes the last action."),
}, },
{ {
@ -250,7 +250,7 @@ window.menus = {
"repeat", "redo", "repeat", "redo",
], ],
enabled: () => redos.length >= 1, enabled: () => redos.length >= 1,
action: ()=> { redo(); }, action: () => { redo(); },
description: localize("Redoes the previously undone action."), description: localize("Redoes the previously undone action."),
}, },
{ {
@ -259,7 +259,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"show history", "history", "show history", "history",
], ],
action: ()=> { show_document_history(); }, action: () => { show_document_history(); },
description: localize("Shows the document history and lets you navigate to states not accessible with Undo or Repeat."), description: localize("Shows the document history and lets you navigate to states not accessible with Undo or Repeat."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -272,7 +272,7 @@ window.menus = {
enabled: () => enabled: () =>
// @TODO: support cutting text with this menu item as well (e.g. for the text tool) // @TODO: support cutting text with this menu item as well (e.g. for the text tool)
!!selection, !!selection,
action: ()=> { action: () => {
edit_cut(true); edit_cut(true);
}, },
description: localize("Cuts the selection and puts it on the Clipboard."), description: localize("Cuts the selection and puts it on the Clipboard."),
@ -286,7 +286,7 @@ window.menus = {
enabled: () => enabled: () =>
// @TODO: support copying text with this menu item as well (e.g. for the text tool) // @TODO: support copying text with this menu item as well (e.g. for the text tool)
!!selection, !!selection,
action: ()=> { action: () => {
edit_copy(true); edit_copy(true);
}, },
description: localize("Copies the selection and puts it on the Clipboard."), description: localize("Copies the selection and puts it on the Clipboard."),
@ -300,7 +300,7 @@ window.menus = {
enabled: () => enabled: () =>
// @TODO: disable if nothing in clipboard or wrong type (if we can access that) // @TODO: disable if nothing in clipboard or wrong type (if we can access that)
true, true,
action: ()=> { action: () => {
edit_paste(true); edit_paste(true);
}, },
description: localize("Inserts the contents of the Clipboard."), description: localize("Inserts the contents of the Clipboard."),
@ -312,7 +312,7 @@ window.menus = {
"delete", "clear selection", "delete selection", "delete selected", "delete selected area", "clear selected area", "erase selected", "erase selected area", "delete", "clear selection", "delete selection", "delete selected", "delete selected area", "clear selected area", "erase selected", "erase selected area",
], ],
enabled: () => !!selection, enabled: () => !!selection,
action: ()=> { delete_selection(); }, action: () => { delete_selection(); },
description: localize("Deletes the selection."), description: localize("Deletes the selection."),
}, },
{ {
@ -323,29 +323,29 @@ window.menus = {
"select the whole image", "select the whole picture", "select the whole drawing", "select the whole canvas", "select the whole document", "select the whole image", "select the whole picture", "select the whole drawing", "select the whole canvas", "select the whole document",
"select the entire image", "select the entire picture", "select the entire drawing", "select the entire canvas", "select the entire document", "select the entire image", "select the entire picture", "select the entire drawing", "select the entire canvas", "select the entire document",
], ],
action: ()=> { select_all(); }, action: () => { select_all(); },
description: localize("Selects everything."), description: localize("Selects everything."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
{ {
item: `${localize("C&opy To")}...`, item: `${localize("C&opy To")}...`,
speech_recognition: [ speech_recognition: [
"copy to file", "copy selection to file", "copy selection to a file", "save selection", "copy to file", "copy selection to file", "copy selection to a file", "save selection",
"save selection as file", "save selection as image", "save selection as picture", "save selection as image file", "save selection as document", "save selection as file", "save selection as image", "save selection as picture", "save selection as image file", "save selection as document",
"save selection as a file", "save selection as a image", "save selection as a picture", "save selection as a image file", "save selection as a document", "save selection as a file", "save selection as a image", "save selection as a picture", "save selection as a image file", "save selection as a document",
"save selection to file", "save selection to image", "save selection to picture", "save selection to image file", "save selection to document", "save selection to file", "save selection to image", "save selection to picture", "save selection to image file", "save selection to document",
"save selection to a file", "save selection to a image", "save selection to a picture", "save selection to a image file", "save selection to a document", "save selection to a file", "save selection to a image", "save selection to a picture", "save selection to a image file", "save selection to a document",
], ],
enabled: () => !!selection, enabled: () => !!selection,
action: ()=> { save_selection_to_file(); }, action: () => { save_selection_to_file(); },
description: localize("Copies the selection to a file."), description: localize("Copies the selection to a file."),
}, },
{ {
item: `${localize("Paste &From")}...`, item: `${localize("Paste &From")}...`,
speech_recognition: [ speech_recognition: [
"paste a file", "paste from a file", "insert a file", "insert an image file", "paste a file", "paste from a file", "insert a file", "insert an image file",
], ],
action: ()=> { choose_file_to_paste(); }, action: () => { choose_file_to_paste(); },
description: localize("Pastes a file into the selection."), description: localize("Pastes a file into the selection."),
} }
], ],
@ -358,7 +358,7 @@ window.menus = {
// @TODO: hide/show // @TODO: hide/show
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
$toolbox.toggle(); $toolbox.toggle();
}, },
check: () => $toolbox.is(":visible"), check: () => $toolbox.is(":visible"),
@ -373,7 +373,7 @@ window.menus = {
// @TODO: hide/show // @TODO: hide/show
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
$colorbox.toggle(); $colorbox.toggle();
}, },
check: () => $colorbox.is(":visible"), check: () => $colorbox.is(":visible"),
@ -387,7 +387,7 @@ window.menus = {
// @TODO: hide/show // @TODO: hide/show
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
$status_area.toggle(); $status_area.toggle();
}, },
check: () => $status_area.is(":visible"), check: () => $status_area.is(":visible"),
@ -422,7 +422,7 @@ window.menus = {
], ],
description: localize("Zooms the picture to 100%."), description: localize("Zooms the picture to 100%."),
enabled: () => magnification !== 1, enabled: () => magnification !== 1,
action: ()=> { action: () => {
set_magnification(1); set_magnification(1);
}, },
}, },
@ -438,7 +438,7 @@ window.menus = {
], ],
description: localize("Zooms the picture to 400%."), description: localize("Zooms the picture to 400%."),
enabled: () => magnification !== 4, enabled: () => magnification !== 4,
action: ()=> { action: () => {
set_magnification(4); set_magnification(4);
}, },
}, },
@ -456,7 +456,7 @@ window.menus = {
"zoom so canvas fits", "zoom so picture fits", "zoom so image fits", "zoom so document fits", "zoom so canvas fits", "zoom so picture fits", "zoom so image fits", "zoom so document fits",
"zoom so whole canvas is visible", "zoom so whole picture is visible", "zoom so whole image is visible", "zoom so whole document is visible", "zoom so whole canvas is visible", "zoom so whole picture is visible", "zoom so whole image is visible", "zoom so whole document is visible",
"zoom so the whole canvas is visible", "zoom so the whole picture is visible", "zoom so the whole image is visible", "zoom so the whole document is visible", "zoom so the whole canvas is visible", "zoom so the whole picture is visible", "zoom so the whole image is visible", "zoom so the whole document is visible",
"fit to window", "fit to view", "fit in window", "fit in view", "fit within window", "fit within view", "fit to window", "fit to view", "fit in window", "fit in view", "fit within window", "fit within view",
"fit picture to window", "fit picture to view", "fit picture in window", "fit picture in view", "fit picture within window", "fit picture within view", "fit picture to window", "fit picture to view", "fit picture in window", "fit picture in view", "fit picture within window", "fit picture within view",
"fit image to window", "fit image to view", "fit image in window", "fit image in view", "fit image within window", "fit image within view", "fit image to window", "fit image to view", "fit image in window", "fit image in view", "fit image within window", "fit image within view",
@ -464,7 +464,7 @@ window.menus = {
"fit document to window", "fit document to view", "fit document in window", "fit document in view", "fit document within window", "fit document within view", "fit document to window", "fit document to view", "fit document in window", "fit document in view", "fit document within window", "fit document within view",
], ],
description: localize("Zooms the picture to fit within the view."), description: localize("Zooms the picture to fit within the view."),
action: ()=> { action: () => {
const rect = $canvas_area[0].getBoundingClientRect(); const rect = $canvas_area[0].getBoundingClientRect();
const margin = 30; // leave a margin so scrollbars won't appear const margin = 30; // leave a margin so scrollbars won't appear
let mag = Math.min( let mag = Math.min(
@ -482,7 +482,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"zoom custom", "custom zoom", "set custom zoom", "set custom zoom level", "zoom to custom level", "zoom to custom", "zoom level", "set zoom level", "zoom custom", "custom zoom", "set custom zoom", "set custom zoom level", "zoom to custom level", "zoom to custom", "zoom level", "set zoom level",
], ],
action: ()=> { show_custom_zoom_window(); }, action: () => { show_custom_zoom_window(); },
}, },
MENU_DIVIDER, MENU_DIVIDER,
{ {
@ -527,7 +527,7 @@ window.menus = {
"show image fullscreen", "show image full-screen", "show image full screen", "show image fullscreen", "show image full-screen", "show image full screen",
// @TODO: exit fullscreen // @TODO: exit fullscreen
], ],
action: ()=> { view_bitmap(); }, action: () => { view_bitmap(); },
description: localize("Displays the entire picture."), description: localize("Displays the entire picture."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -567,7 +567,7 @@ window.menus = {
"flip/rotate", "flip slash rotate", "flip and rotate", "flip or rotate", "flip rotate", "flip/rotate", "flip slash rotate", "flip and rotate", "flip or rotate", "flip rotate",
// @TODO: parameters to command // @TODO: parameters to command
], ],
action: ()=> { image_flip_and_rotate(); }, action: () => { image_flip_and_rotate(); },
description: localize("Flips or rotates the picture or a selection."), description: localize("Flips or rotates the picture or a selection."),
}, },
{ {
@ -579,7 +579,7 @@ window.menus = {
"stretch/skew", "stretch slash skew", "stretch and skew", "stretch or skew", "stretch skew", "stretch/skew", "stretch slash skew", "stretch and skew", "stretch or skew", "stretch skew",
// @TODO: parameters to command // @TODO: parameters to command
], ],
action: ()=> { image_stretch_and_skew(); }, action: () => { image_stretch_and_skew(); },
description: localize("Stretches or skews the picture or a selection."), description: localize("Stretches or skews the picture or a selection."),
}, },
{ {
@ -592,7 +592,7 @@ window.menus = {
"invert image colors", "invert picture colors", "invert drawing colors", "invert image colors", "invert picture colors", "invert drawing colors",
"invert colors of image", "invert colors of picture", "invert colors of drawing", "invert colors of image", "invert colors of picture", "invert colors of drawing",
], ],
action: ()=> { image_invert_colors(); }, action: () => { image_invert_colors(); },
description: localize("Inverts the colors of the picture or a selection."), description: localize("Inverts the colors of the picture or a selection."),
}, },
{ {
@ -606,7 +606,7 @@ window.menus = {
"image size", "picture size", "canvas size", "document size", "page size", "image size", "picture size", "canvas size", "document size", "page size",
"configure image size", "configure picture size", "configure canvas size", "configure document size", "configure page size", "configure image size", "configure picture size", "configure canvas size", "configure document size", "configure page size",
], ],
action: ()=> { image_attributes(); }, action: () => { image_attributes(); },
description: localize("Changes the attributes of the picture."), description: localize("Changes the attributes of the picture."),
}, },
{ {
@ -617,8 +617,8 @@ window.menus = {
// @TODO: erase? // @TODO: erase?
], ],
// (mspaint says "Ctrl+Shft+N") // (mspaint says "Ctrl+Shft+N")
action: ()=> { !selection && clear(); }, action: () => { !selection && clear(); },
enabled: ()=> !selection, enabled: () => !selection,
description: localize("Clears the picture."), description: localize("Clears the picture."),
// action: ()=> { // action: ()=> {
// if (selection) { // if (selection) {
@ -641,7 +641,7 @@ window.menus = {
// @TODO: hide/show / "draw opaque" / "draw transparent"/translucent? // @TODO: hide/show / "draw opaque" / "draw transparent"/translucent?
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
tool_transparent_mode = !tool_transparent_mode; tool_transparent_mode = !tool_transparent_mode;
$G.trigger("option-changed"); $G.trigger("option-changed");
}, },
@ -658,7 +658,7 @@ window.menus = {
"pick custom color", "choose custom color", "pick a custom color", "choose a custom color", "pick custom color", "choose custom color", "pick a custom color", "choose a custom color",
"edit last color", "create new color", "choose new color", "create a new color", "pick a new color", "edit last color", "create new color", "choose new color", "create a new color", "pick a new color",
], ],
action: ()=> { action: () => {
show_edit_colors_window(); show_edit_colors_window();
}, },
description: localize("Creates a new color."), description: localize("Creates a new color."),
@ -668,15 +668,15 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"get colors", "load colors", "load color palette", "load palette", "load color palette file", "load palette file", "load list of colors", "get colors", "load colors", "load color palette", "load palette", "load color palette file", "load palette file", "load list of colors",
], ],
action: async ()=> { action: async () => {
const {file} = await systemHooks.showOpenFileDialog({formats: palette_formats}); const { file } = await systemHooks.showOpenFileDialog({ formats: palette_formats });
AnyPalette.loadPalette(file, (error, new_palette)=> { AnyPalette.loadPalette(file, (error, new_palette) => {
if (error) { if (error) {
show_file_format_errors({ as_palette_error: error }); show_file_format_errors({ as_palette_error: error });
} else { } else {
palette = new_palette.map((color)=> color.toString()); palette = new_palette.map((color) => color.toString());
$colorbox.rebuild_palette(); $colorbox.rebuild_palette();
window.console && console.log(`Loaded palette: ${palette.map(()=> `%c█`).join("")}`, ...palette.map((color)=> `color: ${color};`)); window.console && console.log(`Loaded palette: ${palette.map(() => `%c█`).join("")}`, ...palette.map((color) => `color: ${color};`));
} }
}); });
}, },
@ -687,7 +687,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"save colors", "save list of colors", "save color palette", "save palette", "save color palette file", "save palette file", "save colors", "save list of colors", "save color palette", "save palette", "save color palette file", "save palette file",
], ],
action: ()=> { action: () => {
const ap = new AnyPalette.Palette(); const ap = new AnyPalette.Palette();
ap.name = "JS Paint Saved Colors"; ap.name = "JS Paint Saved Colors";
ap.numberOfColumns = 16; // 14? ap.numberOfColumns = 16; // 14?
@ -703,11 +703,11 @@ window.menus = {
dialogTitle: localize("Save Colors"), dialogTitle: localize("Save Colors"),
defaultFileName: localize("untitled.pal"), defaultFileName: localize("untitled.pal"),
formats: palette_formats, formats: palette_formats,
getBlob: ()=> { getBlob: () => {
const file_content = AnyPalette.writePalette(ap, AnyPalette.formats[format_id]); const file_content = AnyPalette.writePalette(ap, AnyPalette.formats[format_id]);
const blob = new Blob([file_content], {type: "text/plain"}); const blob = new Blob([file_content], { type: "text/plain" });
return new Promise((resolve)=> { return new Promise((resolve) => {
sanity_check_blob(blob, ()=> { sanity_check_blob(blob, () => {
resolve(blob); resolve(blob);
}); });
}); });
@ -724,7 +724,7 @@ window.menus = {
"help topics", "help me", "show help", "help", "show help window", "show help topics", "open help", "help topics", "help me", "show help", "help", "show help window", "show help topics", "open help",
"help viewer", "show help viewer", "open help viewer", "help viewer", "show help viewer", "open help viewer",
], ],
action: ()=> { show_help(); }, action: () => { show_help(); },
description: localize("Displays Help for the current task or command."), description: localize("Displays Help for the current task or command."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -736,7 +736,7 @@ window.menus = {
"application info", "about the application", "application information", "information about the application", "application info", "about the application", "application information", "information about the application",
"who made this", "who did this", "who did this xd", "who made this", "who did this", "who did this xd",
], ],
action: ()=> { show_about_paint(); }, action: () => { show_about_paint(); },
description: localize("Displays information about this application."), description: localize("Displays information about this application."),
//description: localize("Displays program information, version number, and copyright."), //description: localize("Displays program information, version number, and copyright."),
} }
@ -748,7 +748,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
// This is a duplicate menu item (for easy access), so it doesn't need speech recognition data here. // This is a duplicate menu item (for easy access), so it doesn't need speech recognition data here.
], ],
action: ()=> { show_document_history(); }, action: () => { show_document_history(); },
description: localize("Shows the document history and lets you navigate to states not accessible with Undo or Repeat."), description: localize("Shows the document history and lets you navigate to states not accessible with Undo or Repeat."),
}, },
{ {
@ -770,7 +770,7 @@ window.menus = {
"make animation", "make animation of the history", "make animation of the document history", "make animation from the document history", "make animation", "make animation of the history", "make animation of the document history", "make animation from the document history",
"create animation", "create animation of the history", "create animation of the document history", "create animation from the document history", "create animation", "create animation of the history", "create animation of the document history", "create animation from the document history",
], ],
action: ()=> { render_history_as_gif(); }, action: () => { render_history_as_gif(); },
description: localize("Creates an animation from the document history."), description: localize("Creates an animation from the document history."),
}, },
// { // {
@ -854,7 +854,7 @@ window.menus = {
"collaborative", "collaborative",
"collaborating", "collaborating",
], ],
action: ()=> { action: () => {
show_multi_user_setup_dialog(true); show_multi_user_setup_dialog(true);
}, },
description: localize("Starts a new multi-user session from the current document."), description: localize("Starts a new multi-user session from the current document."),
@ -904,7 +904,7 @@ window.menus = {
"start collaboration with empty", "start collaboration with empty",
"start collaborating with empty", "start collaborating with empty",
], ],
action: ()=> { action: () => {
show_multi_user_setup_dialog(false); show_multi_user_setup_dialog(false);
}, },
description: localize("Starts a new multi-user session from an empty document."), description: localize("Starts a new multi-user session from an empty document."),
@ -937,7 +937,7 @@ window.menus = {
"day mode", "switch to day mode", "use day mode", "set mode to day", "set mode day", "switch to day mode", "switch mode to day", "switch mode day", "day mode", "switch to day mode", "use day mode", "set mode to day", "set mode day", "switch to day mode", "switch mode to day", "switch mode day",
"go light", "go bright", "go light", "go bright",
], ],
action: ()=> { action: () => {
set_theme("classic.css"); set_theme("classic.css");
}, },
enabled: () => get_theme() != "classic.css", enabled: () => get_theme() != "classic.css",
@ -954,7 +954,7 @@ window.menus = {
"night mode", "switch to night mode", "use night mode", "set mode to night", "set mode night", "switch to night mode", "switch mode to night", "switch mode night", "night mode", "switch to night mode", "use night mode", "set mode to night", "set mode night", "switch to night mode", "switch mode to night", "switch mode night",
"go dark", "go dim", "go dark", "go dim",
], ],
action: ()=> { action: () => {
set_theme("dark.css"); set_theme("dark.css");
}, },
enabled: () => get_theme() != "dark.css", enabled: () => get_theme() != "dark.css",
@ -965,7 +965,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"modern theme", "switch to modern theme", "use modern theme", "set theme to modern", "set theme modern", "switch to modern theme", "switch theme to modern", "switch theme modern", "modern theme", "switch to modern theme", "use modern theme", "set theme to modern", "set theme modern", "switch to modern theme", "switch theme to modern", "switch theme modern",
], ],
action: ()=> { action: () => {
set_theme("modern.css"); set_theme("modern.css");
}, },
enabled: () => get_theme() != "modern.css", enabled: () => get_theme() != "modern.css",
@ -979,7 +979,7 @@ window.menus = {
"christmas theme", "switch to christmas theme", "use christmas theme", "set theme to christmas", "set theme christmas", "switch to christmas theme", "switch theme to christmas", "switch theme christmas", "christmas theme", "switch to christmas theme", "use christmas theme", "set theme to christmas", "set theme christmas", "switch to christmas theme", "switch theme to christmas", "switch theme christmas",
"hanukkah theme", "switch to hanukkah theme", "use hanukkah theme", "set theme to hanukkah", "set theme hanukkah", "switch to hanukkah theme", "switch theme to hanukkah", "switch theme hanukkah", "hanukkah theme", "switch to hanukkah theme", "use hanukkah theme", "set theme to hanukkah", "set theme hanukkah", "switch to hanukkah theme", "switch theme to hanukkah", "switch theme hanukkah",
], ],
action: ()=> { action: () => {
set_theme("winter.css"); set_theme("winter.css");
}, },
enabled: () => get_theme() != "winter.css", enabled: () => get_theme() != "winter.css",
@ -1023,12 +1023,12 @@ window.menus = {
"welcome devil", "welcome the devil", "welcome devil theme", "welcome the devil theme", "welcome devil", "welcome the devil", "welcome devil theme", "welcome the devil theme",
"I beseech thee", "I entreat thee", "I summon thee", "I call upon thy name", "I call upon thine name", "Lord Satan", "hail Satan", "hail Lord Satan", "O Mighty Satan", "Oh Mighty Satan", "I beseech thee", "I entreat thee", "I summon thee", "I call upon thy name", "I call upon thine name", "Lord Satan", "hail Satan", "hail Lord Satan", "O Mighty Satan", "Oh Mighty Satan",
"In nomine Dei nostri Satanas Luciferi Excelsi", "Rege Satanas", "Ave Satanas","Rege Satana", "Ave Satana", "In nomine Dei nostri Satanas Luciferi Excelsi", "Rege Satanas", "Ave Satanas", "Rege Satana", "Ave Satana",
"go demonic", "go daemonic", "go occult", "666", "go demonic", "go daemonic", "go occult", "666",
"begin ritual", "begin the ritual", "begin a ritual", "begin ritual", "begin the ritual", "begin a ritual",
"start ritual", "start the ritual", "start a ritual", "start ritual", "start the ritual", "start a ritual",
], ],
action: ()=> { action: () => {
set_theme("occult.css"); set_theme("occult.css");
}, },
enabled: () => get_theme() != "occult.css", enabled: () => get_theme() != "occult.css",
@ -1038,10 +1038,10 @@ window.menus = {
}, },
{ {
item: "🌍 " + localize("&Language"), item: "🌍 " + localize("&Language"),
submenu: available_languages.map((available_language)=> ( submenu: available_languages.map((available_language) => (
{ {
item: get_language_emoji(available_language) + " " + get_language_endonym(available_language), item: get_language_emoji(available_language) + " " + get_language_endonym(available_language),
action: ()=> { action: () => {
set_language(available_language); set_language(available_language);
}, },
enabled: () => get_language() != available_language, enabled: () => get_language() != available_language,
@ -1077,7 +1077,7 @@ window.menus = {
"eye gaze off", "eye gaze off",
"start eye gaze", "start eye gaze",
"stop eye gaze", "stop eye gaze",
"toggle eye gazing", "toggle eye gazing",
"enable eye gazing", "enable eye gazing",
"disable eye gazing", "disable eye gazing",
@ -1092,7 +1092,7 @@ window.menus = {
"stop eye gazing", "stop eye gazing",
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
if (location.hash.match(/eye-gaze-mode/i)) { if (location.hash.match(/eye-gaze-mode/i)) {
// @TODO: confirmation dialog that you could cancel with dwell clicking! // @TODO: confirmation dialog that you could cancel with dwell clicking!
// if (confirm("This will disable eye gaze mode.")) { // if (confirm("This will disable eye gaze mode.")) {
@ -1102,7 +1102,7 @@ window.menus = {
change_url_param("eye-gaze-mode", true); change_url_param("eye-gaze-mode", true);
} }
}, },
check: ()=> { check: () => {
return location.hash.match(/eye-gaze-mode/i); return location.hash.match(/eye-gaze-mode/i);
}, },
}, },
@ -1115,18 +1115,18 @@ window.menus = {
"disable speech recognition", "disable speech recognition mode", "turn off speech recognition", "turn off speech recognition mode", "leave speech recognition mode", "exit speech recognition mode", "disable speech recognition", "disable speech recognition mode", "turn off speech recognition", "turn off speech recognition mode", "leave speech recognition mode", "exit speech recognition mode",
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
if (location.hash.match(/speech-recognition-mode/i)) { if (location.hash.match(/speech-recognition-mode/i)) {
change_url_param("speech-recognition-mode", false); change_url_param("speech-recognition-mode", false);
} else { } else {
change_url_param("speech-recognition-mode", true); change_url_param("speech-recognition-mode", true);
} }
}, },
check: ()=> { check: () => {
return window.speech_recognition_active; return window.speech_recognition_active;
}, },
}, },
enabled: ()=> window.speech_recognition_available, enabled: () => window.speech_recognition_available,
description: localize("Controls the application with voice commands."), description: localize("Controls the application with voice commands."),
}, },
{ {
@ -1141,7 +1141,7 @@ window.menus = {
// @TODO: "use a vertical/horizontal color box", "place palette on the left", "make palette tall/wide", etc. // @TODO: "use a vertical/horizontal color box", "place palette on the left", "make palette tall/wide", etc.
], ],
checkbox: { checkbox: {
toggle: ()=> { toggle: () => {
if (location.hash.match(/eye-gaze-mode/i)) { if (location.hash.match(/eye-gaze-mode/i)) {
// @TODO: confirmation dialog that you could cancel with dwell clicking! // @TODO: confirmation dialog that you could cancel with dwell clicking!
// if (confirm("This will disable eye gaze mode.")) { // if (confirm("This will disable eye gaze mode.")) {
@ -1156,11 +1156,11 @@ window.menus = {
change_url_param("vertical-color-box-mode", true); change_url_param("vertical-color-box-mode", true);
} }
}, },
check: ()=> { check: () => {
return location.hash.match(/vertical-color-box-mode|eye-gaze-mode/i); return location.hash.match(/vertical-color-box-mode|eye-gaze-mode/i);
}, },
}, },
enabled: ()=> { enabled: () => {
return !location.hash.match(/eye-gaze-mode/i); return !location.hash.match(/eye-gaze-mode/i);
}, },
description: localize("Arranges the color box vertically."), description: localize("Arranges the color box vertically."),
@ -1171,7 +1171,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
// This is a duplicate menu item (for easy access), so it doesn't need speech recognition data here. // This is a duplicate menu item (for easy access), so it doesn't need speech recognition data here.
], ],
action: ()=> { manage_storage(); }, action: () => { manage_storage(); },
description: localize("Manages storage of previously created or opened pictures."), description: localize("Manages storage of previously created or opened pictures."),
}, },
MENU_DIVIDER, MENU_DIVIDER,
@ -1184,7 +1184,7 @@ window.menus = {
"what's new", "new features", "what's new", "new features",
"show news", "show news update", "news update", "show news", "show news update", "news update",
], ],
action: ()=> { show_news(); }, action: () => { show_news(); },
description: localize("Shows news about JS Paint."), description: localize("Shows news about JS Paint."),
}, },
{ {
@ -1192,7 +1192,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"repo on github", "project on github", "show the source code", "show source code", "repo on github", "project on github", "show the source code", "show source code",
], ],
action: ()=> { window.open("https://github.com/1j01/jspaint"); }, action: () => { window.open("https://github.com/1j01/jspaint"); },
description: localize("Shows the project on GitHub."), description: localize("Shows the project on GitHub."),
}, },
{ {
@ -1200,7 +1200,7 @@ window.menus = {
speech_recognition: [ speech_recognition: [
"donate", "make a monetary contribution", "donate", "make a monetary contribution",
], ],
action: ()=> { window.open("https://www.paypal.me/IsaiahOdhner"); }, action: () => { window.open("https://www.paypal.me/IsaiahOdhner"); },
description: localize("Supports the project."), description: localize("Supports the project."),
}, },
], ],
@ -1208,7 +1208,7 @@ window.menus = {
for (const [top_level_menu_key, menu] of Object.entries(menus)) { for (const [top_level_menu_key, menu] of Object.entries(menus)) {
const top_level_menu_name = top_level_menu_key.replace(/&/, ""); const top_level_menu_name = top_level_menu_key.replace(/&/, "");
const add_literal_navigation_speech_recognition = (menu, ancestor_names)=> { const add_literal_navigation_speech_recognition = (menu, ancestor_names) => {
for (const menu_item of menu) { for (const menu_item of menu) {
if (menu_item !== MENU_DIVIDER) { if (menu_item !== MENU_DIVIDER) {
const menu_item_name = menu_item.item.replace(/&|\.\.\.|\(|\)/g, ""); const menu_item_name = menu_item.item.replace(/&|\.\.\.|\(|\)/g, "");
@ -1223,7 +1223,7 @@ for (const [top_level_menu_key, menu] of Object.entries(menus)) {
menu_item_name.replace(/\//, " slash "), menu_item_name.replace(/\//, " slash "),
]; ];
} }
menu_item_matchers = menu_item_matchers.map((menu_item_matcher)=> { menu_item_matchers = menu_item_matchers.map((menu_item_matcher) => {
return `${ancestor_names} ${menu_item_matcher}`; return `${ancestor_names} ${menu_item_matcher}`;
}); });
menu_item.speech_recognition = (menu_item.speech_recognition || []).concat(menu_item_matchers); menu_item.speech_recognition = (menu_item.speech_recognition || []).concat(menu_item_matchers);

View File

@ -1,7 +1,7 @@
(() => { (() => {
const log = (...args)=> { const log = (...args) => {
window.console && console.log(...args); window.console && console.log(...args);
}; };
@ -10,8 +10,8 @@
localStorage._available = true; localStorage._available = true;
localStorageAvailable = localStorage._available; localStorageAvailable = localStorage._available;
delete localStorage._available; delete localStorage._available;
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) { }
// @TODO: keep other data in addition to the image data // @TODO: keep other data in addition to the image data
// such as the file_name and other state // such as the file_name and other state
@ -19,29 +19,28 @@
// I could have the image in one storage slot and the state in another // I could have the image in one storage slot and the state in another
const match_threshold = 1; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184 const match_threshold = 1; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184
const canvas_has_any_apparent_image_data = ()=> const canvas_has_any_apparent_image_data = () =>
main_canvas.ctx.getImageData(0, 0, main_canvas.width, main_canvas.height).data.some((v)=> v > match_threshold); main_canvas.ctx.getImageData(0, 0, main_canvas.width, main_canvas.height).data.some((v) => v > match_threshold);
let $recovery_window; let $recovery_window;
function show_recovery_window(no_longer_blank) { function show_recovery_window(no_longer_blank) {
$recovery_window && $recovery_window.close(); $recovery_window && $recovery_window.close();
const $w = $recovery_window = $DialogWindow(); const $w = $recovery_window = $DialogWindow();
$w.on("close", ()=> { $w.on("close", () => {
$recovery_window = null; $recovery_window = null;
}); });
$w.title("Recover Document"); $w.title("Recover Document");
let backup_impossible = false; let backup_impossible = false;
try{window.localStorage}catch(e){backup_impossible = true;} try { window.localStorage } catch (e) { backup_impossible = true; }
$w.$main.append($(` $w.$main.append($(`
<h1>Woah!</h1> <h1>Woah!</h1>
<p>Your browser may have cleared the canvas due to memory usage.</p> <p>Your browser may have cleared the canvas due to memory usage.</p>
<p>Undo to recover the document, and remember to save with <b>File > Save</b>!</p> <p>Undo to recover the document, and remember to save with <b>File > Save</b>!</p>
${ ${backup_impossible ?
backup_impossible ? "<p><b>Note:</b> No automatic backup is possible unless you enable Cookies in your browser.</p>"
"<p><b>Note:</b> No automatic backup is possible unless you enable Cookies in your browser.</p>" : (
: ( no_longer_blank ?
no_longer_blank ? `<p>
`<p>
<b>Note:</b> normally a backup is saved automatically,<br> <b>Note:</b> normally a backup is saved automatically,<br>
but autosave is paused while this dialog is open<br> but autosave is paused while this dialog is open<br>
to avoid overwriting the (singular) backup. to avoid overwriting the (singular) backup.
@ -49,26 +48,26 @@
<p> <p>
(See <b>File &gt; Manage Storage</b> to view backups.) (See <b>File &gt; Manage Storage</b> to view backups.)
</p>` </p>`
: "" : ""
) )
} }
} }
`)); `));
const $undo = $w.$Button("Undo", ()=> { const $undo = $w.$Button("Undo", () => {
undo(); undo();
}); });
const $redo = $w.$Button("Redo", ()=> { const $redo = $w.$Button("Redo", () => {
redo(); redo();
}); });
const update_buttons_disabled = ()=> { const update_buttons_disabled = () => {
$undo.attr("disabled", undos.length < 1); $undo.attr("disabled", undos.length < 1);
$redo.attr("disabled", redos.length < 1); $redo.attr("disabled", redos.length < 1);
}; };
$G.on("session-update.session-hook", update_buttons_disabled); $G.on("session-update.session-hook", update_buttons_disabled);
update_buttons_disabled(); update_buttons_disabled();
$w.$Button(localize("Close"), ()=> { $w.$Button(localize("Close"), () => {
$w.close(); $w.close();
}); });
$w.center(); $w.center();
@ -94,7 +93,7 @@
last_undos_length = undos.length; last_undos_length = undos.length;
return save_paused; return save_paused;
} }
class LocalSession { class LocalSession {
constructor(session_id) { constructor(session_id) {
this.id = session_id; this.id = session_id;
@ -188,7 +187,7 @@
// Unused // Unused
user.color_transparent = `hsla(${user.hue}, ${user.saturation}%, ${user.lightness}%, 0.5)`; user.color_transparent = `hsla(${user.hue}, ${user.saturation}%, ${user.lightness}%, 0.5)`;
// (@TODO) The color (that may be) used in the toolbar indicating to other users it is selected by this user // (@TODO) The color (that may be) used in the toolbar indicating to other users it is selected by this user
user.color_desaturated = `hsla(${user.hue}, ${~~(user.saturation*0.4)}%, ${user.lightness}%, 0.8)`; user.color_desaturated = `hsla(${user.hue}, ${~~(user.saturation * 0.4)}%, ${user.lightness}%, 0.8)`;
// The image used for other people's cursors // The image used for other people's cursors
@ -248,7 +247,7 @@
// "so for now I'm showing this message regardless of whether it's working.</p>" + // "so for now I'm showing this message regardless of whether it's working.</p>" +
// "<p>If you're interested in using multiuser mode, please thumbs-up " + // "<p>If you're interested in using multiuser mode, please thumbs-up " +
// "<a href='https://github.com/1j01/jspaint/issues/68'>this issue</a> to show interest, and/or subscribe for updates.</p>" // "<a href='https://github.com/1j01/jspaint/issues/68'>this issue</a> to show interest, and/or subscribe for updates.</p>"
// Wrap the Firebase API because they don't // Wrap the Firebase API because they don't
// provide a great way to clean up event listeners // provide a great way to clean up event listeners
const _fb_on = (fb, event_type, callback, error_callback) => { const _fb_on = (fb, event_type, callback, error_callback) => {
@ -359,7 +358,7 @@
}; };
this.write_canvas_to_database_soon = debounce(this.write_canvas_to_database_immediately, 100); this.write_canvas_to_database_soon = debounce(this.write_canvas_to_database_immediately, 100);
let ignore_session_update = false; let ignore_session_update = false;
$G.on("session-update.session-hook", ()=> { $G.on("session-update.session-hook", () => {
if (ignore_session_update) { if (ignore_session_update) {
log("(Ignore session-update from Sync Session undoable)"); log("(Ignore session-update from Sync Session undoable)");
return; return;
@ -388,13 +387,13 @@
const test_canvas = make_canvas(img); const test_canvas = make_canvas(img);
const image_data_remote = test_canvas.ctx.getImageData(0, 0, test_canvas.width, test_canvas.height); const image_data_remote = test_canvas.ctx.getImageData(0, 0, test_canvas.width, test_canvas.height);
const image_data_local = main_ctx.getImageData(0, 0, main_canvas.width, main_canvas.height); const image_data_local = main_ctx.getImageData(0, 0, main_canvas.width, main_canvas.height);
if (!image_data_match(image_data_remote, image_data_local, 5)) { if (!image_data_match(image_data_remote, image_data_local, 5)) {
ignore_session_update = true; ignore_session_update = true;
undoable({ undoable({
name: "Sync Session", name: "Sync Session",
icon: get_help_folder_icon("p_database.png"), icon: get_help_folder_icon("p_database.png"),
}, ()=> { }, () => {
// Write the image data to the canvas // Write the image data to the canvas
main_ctx.copy(img); main_ctx.copy(img);
$canvas_area.trigger("resize"); $canvas_area.trigger("resize");
@ -432,7 +431,7 @@
away: false, away: false,
}); });
}); });
$G.on("blur.session-hook", ()=> { $G.on("blur.session-hook", () => {
this.fb_user.child("cursor").update({ this.fb_user.child("cursor").update({
away: true, away: true,
}); });
@ -498,45 +497,45 @@
let current_session; let current_session;
const end_current_session = () => { const end_current_session = () => {
if(current_session){ if (current_session) {
log("Ending current session"); log("Ending current session");
current_session.end(); current_session.end();
current_session = null; current_session = null;
} }
}; };
const generate_session_id = () => (Math.random()*(2 ** 32)).toString(16).replace(".", ""); const generate_session_id = () => (Math.random() * (2 ** 32)).toString(16).replace(".", "");
const update_session_from_location_hash = () => { const update_session_from_location_hash = () => {
const session_match = location.hash.match(/^#?(?:.*,)?(session|local):(.*)$/i); const session_match = location.hash.match(/^#?(?:.*,)?(session|local):(.*)$/i);
const load_from_url_match = location.hash.match(/^#?(?:.*,)?(load):(.*)$/i); const load_from_url_match = location.hash.match(/^#?(?:.*,)?(load):(.*)$/i);
if(session_match){ if (session_match) {
const local = session_match[1].toLowerCase() === "local"; const local = session_match[1].toLowerCase() === "local";
const session_id = session_match[2]; const session_id = session_match[2];
if(session_id === ""){ if (session_id === "") {
log("Invalid session ID; session ID cannot be empty"); log("Invalid session ID; session ID cannot be empty");
end_current_session(); end_current_session();
}else if(!local && session_id.match(/[./[\]#$]/)){ } else if (!local && session_id.match(/[./[\]#$]/)) {
log("Session ID is not a valid Firebase location; it cannot contain any of ./[]#$"); log("Session ID is not a valid Firebase location; it cannot contain any of ./[]#$");
end_current_session(); end_current_session();
}else if(!session_id.match(/[-0-9A-Za-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02af\u1d00-\u1d25\u1d62-\u1d65\u1d6b-\u1d77\u1d79-\u1d9a\u1e00-\u1eff\u2090-\u2094\u2184-\u2184\u2488-\u2490\u271d-\u271d\u2c60-\u2c7c\u2c7e-\u2c7f\ua722-\ua76f\ua771-\ua787\ua78b-\ua78c\ua7fb-\ua7ff\ufb00-\ufb06]+/)){ } else if (!session_id.match(/[-0-9A-Za-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02af\u1d00-\u1d25\u1d62-\u1d65\u1d6b-\u1d77\u1d79-\u1d9a\u1e00-\u1eff\u2090-\u2094\u2184-\u2184\u2488-\u2490\u271d-\u271d\u2c60-\u2c7c\u2c7e-\u2c7f\ua722-\ua76f\ua771-\ua787\ua78b-\ua78c\ua7fb-\ua7ff\ufb00-\ufb06]+/)) {
log("Invalid session ID; it must consist of 'alphanumeric-esque' characters"); log("Invalid session ID; it must consist of 'alphanumeric-esque' characters");
end_current_session(); end_current_session();
}else if( } else if (
current_session && current_session.id === session_id && current_session && current_session.id === session_id &&
local === (current_session instanceof LocalSession) local === (current_session instanceof LocalSession)
){ ) {
log("Hash changed but the session ID and session type are the same"); log("Hash changed but the session ID and session type are the same");
}else{ } else {
// @TODO: Ask if you want to save before starting a new session // @TODO: Ask if you want to save before starting a new session
end_current_session(); end_current_session();
if(local){ if (local) {
log(`Starting a new LocalSession, ID: ${session_id}`); log(`Starting a new LocalSession, ID: ${session_id}`);
current_session = new LocalSession(session_id); current_session = new LocalSession(session_id);
}else{ } else {
log(`Starting a new MultiUserSession, ID: ${session_id}`); log(`Starting a new MultiUserSession, ID: ${session_id}`);
current_session = new MultiUserSession(session_id); current_session = new MultiUserSession(session_id);
} }
} }
}else if(load_from_url_match){ } else if (load_from_url_match) {
const url = decodeURIComponent(load_from_url_match[2]); const url = decodeURIComponent(load_from_url_match[2]);
const uris = get_uris(url); const uris = get_uris(url);
@ -554,11 +553,11 @@
open_from_image_info(info, null, null, true, true); open_from_image_info(info, null, null, true, true);
}, show_resource_load_error_message); }, show_resource_load_error_message);
}else{ } else {
log("No session ID in hash"); log("No session ID in hash");
const old_hash = location.hash; const old_hash = location.hash;
end_current_session(); end_current_session();
change_url_param("local", generate_session_id(), {replace_history_state: true}); change_url_param("local", generate_session_id(), { replace_history_state: true });
log("After replaceState:", location.hash); log("After replaceState:", location.hash);
if (old_hash === location.hash) { if (old_hash === location.hash) {
// e.g. on Wayback Machine // e.g. on Wayback Machine

View File

@ -1,4 +1,4 @@
(()=>{ (() => {
let seed = 4; // chosen later let seed = 4; // chosen later
@ -15,8 +15,8 @@ window.simulatingGestures = false;
let gestureTimeoutID; let gestureTimeoutID;
let periodicGesturesTimeoutID; let periodicGesturesTimeoutID;
let choose = (array)=> array[~~(seededRandom() * array.length)]; let choose = (array) => array[~~(seededRandom() * array.length)];
let isAnyMenuOpen = ()=> $(".menu-button.active").length > 0; let isAnyMenuOpen = () => $(".menu-button.active").length > 0;
let cursor_image = new Image(); let cursor_image = new Image();
cursor_image.src = "images/cursors/default.png"; cursor_image.src = "images/cursors/default.png";
@ -32,7 +32,7 @@ $cursor.css({
transition: "opacity 0.5s", transition: "opacity 0.5s",
}); });
window.simulateRandomGesture = (callback, {shift, shiftToggleChance=0.01, secondary, secondaryToggleChance, target=main_canvas}) => { window.simulateRandomGesture = (callback, { shift, shiftToggleChance = 0.01, secondary, secondaryToggleChance, target = main_canvas }) => {
let startWithinRect = target.getBoundingClientRect(); let startWithinRect = target.getBoundingClientRect();
let canvasAreaRect = $canvas_area[0].getBoundingClientRect(); let canvasAreaRect = $canvas_area[0].getBoundingClientRect();
@ -45,7 +45,7 @@ window.simulateRandomGesture = (callback, {shift, shiftToggleChance=0.01, second
$cursor.appendTo($app); $cursor.appendTo($app);
let triggerMouseEvent = (type, point) => { let triggerMouseEvent = (type, point) => {
if (isAnyMenuOpen()) { if (isAnyMenuOpen()) {
return; return;
} }
@ -122,7 +122,7 @@ window.simulateRandomGesture = (callback, {shift, shiftToggleChance=0.01, second
point.y += startPointY; point.y += startPointY;
return point; return point;
}; };
triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor
triggerMouseEvent("pointerdown", pointForTime(t)); triggerMouseEvent("pointerdown", pointForTime(t));
let move = () => { let move = () => {
@ -135,7 +135,7 @@ window.simulateRandomGesture = (callback, {shift, shiftToggleChance=0.01, second
} }
if (t > 1) { if (t > 1) {
triggerMouseEvent("pointerup", pointForTime(t)); triggerMouseEvent("pointerup", pointForTime(t));
$cursor.remove(); $cursor.remove();
if (callback) { if (callback) {
@ -161,14 +161,14 @@ window.simulateRandomGesturesPeriodically = () => {
window.console && console.log("Using seed:", seed); window.console && console.log("Using seed:", seed);
window.console && console.log("Note: Seeds are not guaranteed to work with different versions of the app, but within the same version it should produce the same results given the same starting document & other state & NO interference... except for airbrush randomness"); window.console && console.log("Note: Seeds are not guaranteed to work with different versions of the app, but within the same version it should produce the same results given the same starting document & other state & NO interference... except for airbrush randomness");
window.console && console.log(`To use this seed: window.console && console.log(`To use this seed:
window.drawRandomlySeed = ${seed}; window.drawRandomlySeed = ${seed};
document.body.style.width = "${getComputedStyle(document.body).width}"; document.body.style.width = "${getComputedStyle(document.body).width}";
document.body.style.height = "${getComputedStyle(document.body).height}"; document.body.style.height = "${getComputedStyle(document.body).height}";
simulateRandomGesturesPeriodically(); simulateRandomGesturesPeriodically();
delete window.drawRandomlySeed; delete window.drawRandomlySeed;
`); `);
let delayBetweenGestures = 500; let delayBetweenGestures = 500;
let shiftStart = false; let shiftStart = false;
@ -187,8 +187,8 @@ window.simulateRandomGesturesPeriodically = () => {
// scroll randomly absolutely initially so the starting scroll doesn't play into whether a seed reproduces // scroll randomly absolutely initially so the starting scroll doesn't play into whether a seed reproduces
$canvas_area.scrollTop($canvas_area.width() * seededRandom()); $canvas_area.scrollTop($canvas_area.width() * seededRandom());
$canvas_area.scrollLeft($canvas_area.height() * seededRandom()); $canvas_area.scrollLeft($canvas_area.height() * seededRandom());
let _simulateRandomGesture = (callback)=> { let _simulateRandomGesture = (callback) => {
window.simulateRandomGesture(callback, { window.simulateRandomGesture(callback, {
shift: shiftStart, shift: shiftStart,
shiftToggleChance, shiftToggleChance,
@ -212,7 +212,7 @@ window.simulateRandomGesturesPeriodically = () => {
} }
if (seededRandom() < switchToolsChance) { if (seededRandom() < switchToolsChance) {
let multiToolsPlz = seededRandom() < multiToolsChance; let multiToolsPlz = seededRandom() < multiToolsChance;
$(choose($(".tool, tool-button"))).trigger($.Event("click", {shiftKey: multiToolsPlz})); $(choose($(".tool, tool-button"))).trigger($.Event("click", { shiftKey: multiToolsPlz }));
} }
if (seededRandom() < pickToolOptionsChance) { if (seededRandom() < pickToolOptionsChance) {
$(choose($(".tool-options *"))).trigger("click"); $(choose($(".tool-options *"))).trigger("click");
@ -222,9 +222,9 @@ window.simulateRandomGesturesPeriodically = () => {
let secondary = seededRandom() < 0.5; let secondary = seededRandom() < 0.5;
const colorButton = choose($(".swatch, .color-button")); const colorButton = choose($(".swatch, .color-button"));
$(colorButton) $(colorButton)
.trigger($.Event("pointerdown", {button: secondary ? 2 : 0})) .trigger($.Event("pointerdown", { button: secondary ? 2 : 0 }))
.trigger($.Event("click", {button: secondary ? 2 : 0})) .trigger($.Event("click", { button: secondary ? 2 : 0 }))
.trigger($.Event("pointerup", {button: secondary ? 2 : 0})); .trigger($.Event("pointerup", { button: secondary ? 2 : 0 }));
} }
if (seededRandom() < scrollChance) { if (seededRandom() < scrollChance) {
let scrollAmount = (seededRandom() * 2 - 1) * 700; let scrollAmount = (seededRandom() * 2 - 1) * 700;
@ -235,7 +235,7 @@ window.simulateRandomGesturesPeriodically = () => {
} }
} }
periodicGesturesTimeoutID = setTimeout(() => { periodicGesturesTimeoutID = setTimeout(() => {
_simulateRandomGesture(()=> { _simulateRandomGesture(() => {
if (selection && seededRandom() < dragSelectionChance) { if (selection && seededRandom() < dragSelectionChance) {
window.simulateRandomGesture(waitThenGo, { window.simulateRandomGesture(waitThenGo, {
shift: shiftStart, shift: shiftStart,

View File

@ -1,4 +1,4 @@
(function() { (function () {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList; const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
@ -1251,16 +1251,16 @@ const recognitionFixes = [
// spell-checker:enable // spell-checker:enable
]; ];
const colorNames = [ 'aqua', 'azure', 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral', 'crimson', 'cyan', 'fuchsia', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'indigo', 'ivory', 'khaki', 'lavender', 'lime', 'linen', 'magenta', 'maroon', 'moccasin', 'navy', 'olive', 'orange', 'orchid', 'peru', 'pink', 'plum', 'purple', 'red', 'salmon', 'sienna', 'silver', 'snow', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'white', 'yellow']; const colorNames = ['aqua', 'azure', 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral', 'crimson', 'cyan', 'fuchsia', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'indigo', 'ivory', 'khaki', 'lavender', 'lime', 'linen', 'magenta', 'maroon', 'moccasin', 'navy', 'olive', 'orange', 'orchid', 'peru', 'pink', 'plum', 'purple', 'red', 'salmon', 'sienna', 'silver', 'snow', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'white', 'yellow'];
const toolNames = tools.map((tool)=> tool.speech_recognition).flat(); const toolNames = tools.map((tool) => tool.speech_recognition).flat();
// @TODO: select foreground/background/ternary color specifically // @TODO: select foreground/background/ternary color specifically
// @TODO: switch colors / swap colors / swap foreground and background colors // @TODO: switch colors / swap colors / swap foreground and background colors
// @TODO: zoom in/out / increase/decrease magnification, zoom to 20x / 5% etc., zoom out all the way (actual size or best fit if it's too big), actual size // @TODO: zoom in/out / increase/decrease magnification, zoom to 20x / 5% etc., zoom out all the way (actual size or best fit if it's too big), actual size
// @TODO: "convert image to black-and-white" / "convert image to monochrome" / "make image monochrome", "increase/decrease threshold" / "more white" / "more black" // @TODO: "convert image to black-and-white" / "convert image to monochrome" / "make image monochrome", "increase/decrease threshold" / "more white" / "more black"
// @TODO: in Image Attributes, "Color"/"Colors"/"Not black and white" for Colors // @TODO: in Image Attributes, "Color"/"Colors"/"Not black and white" for Colors
// @TODO: select tool options like selection opacity and brush sizes // @TODO: select tool options like selection opacity and brush sizes
// opaque/transparent/translucent/see-through selection / make selection opaque/transparent/translucent/see-through // opaque/transparent/translucent/see-through selection / make selection opaque/transparent/translucent/see-through
// "increase size"(too vague) / "increase brush size" / "increase eraser size" / "larger eraser" / "enlarge eraser" // "increase size"(too vague) / "increase brush size" / "increase eraser size" / "larger eraser" / "enlarge eraser"
// @TODO: Is there a way to enable the grammar only as a hint, non-restrictively? // @TODO: Is there a way to enable the grammar only as a hint, non-restrictively?
// Construct a grammar that just contains an English dictionary, and set it as lower weight? // Construct a grammar that just contains an English dictionary, and set it as lower weight?
@ -1290,13 +1290,13 @@ recognition.maxAlternatives = 1;
window.speech_recognition_active = false; window.speech_recognition_active = false;
window.enable_speech_recognition = function() { window.enable_speech_recognition = function () {
if (!window.speech_recognition_active) { if (!window.speech_recognition_active) {
window.speech_recognition_active = true; window.speech_recognition_active = true;
recognition.start(); recognition.start();
} }
}; };
window.disable_speech_recognition = function() { window.disable_speech_recognition = function () {
if (window.speech_recognition_active) { if (window.speech_recognition_active) {
window.speech_recognition_active = false; window.speech_recognition_active = false;
recognition.stop(); recognition.stop();
@ -1321,7 +1321,7 @@ function fix_up_speech_recognition(command) {
} }
return command; return command;
} }
recognition.onresult = function(event) { recognition.onresult = function (event) {
if (document.visibilityState !== "visible") { if (document.visibilityState !== "visible") {
return; return;
} }
@ -1343,12 +1343,11 @@ recognition.onresult = function(event) {
const interpretations = interpret_command(command, true); const interpretations = interpret_command(command, true);
if (interpretations.length) { if (interpretations.length) {
const interpretation = choose_interpretation(interpretations); const interpretation = choose_interpretation(interpretations);
$status_text.html(`Speech:&nbsp;<span style="white-space: pre;">${ $status_text.html(`Speech:&nbsp;<span style="white-space: pre;">${command.replace(
command.replace( new RegExp(escapeRegExp(interpretation.match_text), "i"),
new RegExp(escapeRegExp(interpretation.match_text), "i"), (important_text) => `<b>${important_text}</b>`,
(important_text)=> `<b>${important_text}</b>`, )
) }</span>`);
}</span>`);
console.log(`Interpreting command "${command}" as`, interpretation); console.log(`Interpreting command "${command}" as`, interpretation);
interpretation.exec(); interpretation.exec();
} else { } else {
@ -1357,35 +1356,35 @@ recognition.onresult = function(event) {
} }
}; };
recognition.onspeechend = function() { recognition.onspeechend = function () {
recognition.addEventListener("end", ()=> { recognition.addEventListener("end", () => {
recognition.start(); recognition.start();
}, {once: true}); }, { once: true });
recognition.stop(); recognition.stop();
}; };
recognition.onnomatch = function(event) { recognition.onnomatch = function (event) {
if (document.visibilityState !== "visible") { if (document.visibilityState !== "visible") {
return; return;
} }
$status_text.text("Speech not recognized."); $status_text.text("Speech not recognized.");
}; };
recognition.onstart = function(event) { recognition.onstart = function (event) {
window.speech_recognition_active = true; window.speech_recognition_active = true;
}; };
recognition.onend = function(event) { recognition.onend = function (event) {
window.speech_recognition_active = false; window.speech_recognition_active = false;
}; };
recognition.onerror = function(event) { recognition.onerror = function (event) {
if (event.error.toString().match(/no-speech/)) { if (event.error.toString().match(/no-speech/)) {
try { try {
recognition.start(); recognition.start();
} catch(error) { } catch (error) {
recognition.addEventListener("end", ()=> { recognition.addEventListener("end", () => {
recognition.start(); recognition.start();
}, {once: true}); }, { once: true });
} }
} else { } else {
$status_text.text('Error occurred in speech recognition: ' + event.error); $status_text.text('Error occurred in speech recognition: ' + event.error);
@ -1406,22 +1405,22 @@ function choose_interpretation(interpretations) {
interpretation.prioritize interpretation.prioritize
) { ) {
best_interpretation = interpretation; best_interpretation = interpretation;
} }
} }
return best_interpretation; return best_interpretation;
} }
window.interpret_command = (input_text, default_to_entering_text)=> { window.interpret_command = (input_text, default_to_entering_text) => {
const interpretations = []; const interpretations = [];
const add_interpretation = (interpretation)=> { const add_interpretation = (interpretation) => {
interpretations.push(interpretation); interpretations.push(interpretation);
}; };
for (const color of colorNames) { for (const color of colorNames) {
if (` ${input_text} `.toLowerCase().indexOf(` ${color.toLowerCase()} `) !== -1) { if (` ${input_text} `.toLowerCase().indexOf(` ${color.toLowerCase()} `) !== -1) {
add_interpretation({ add_interpretation({
match_text: color, match_text: color,
exec: ((color)=> ()=> { exec: ((color) => () => {
selected_colors.foreground = color; selected_colors.foreground = color;
$G.trigger("option-changed"); $G.trigger("option-changed");
})(color), })(color),
@ -1436,7 +1435,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({ add_interpretation({
match_text: select_tool_match[0], match_text: select_tool_match[0],
tool_id: tool.id, tool_id: tool.id,
exec: ((tool)=> ()=> { exec: ((tool) => () => {
select_tool(tool); select_tool(tool);
})(tool), })(tool),
}); });
@ -1445,7 +1444,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
} }
const all_menu_items = []; const all_menu_items = [];
const collect_menu_items = (menu)=> { const collect_menu_items = (menu) => {
for (const menu_item of menu) { for (const menu_item of menu) {
if (menu_item !== MENU_DIVIDER) { if (menu_item !== MENU_DIVIDER) {
all_menu_items.push(menu_item); all_menu_items.push(menu_item);
@ -1463,7 +1462,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (` ${input_text} `.toLowerCase().indexOf(` ${menu_item_phrase.toLowerCase()} `) !== -1) { if (` ${input_text} `.toLowerCase().indexOf(` ${menu_item_phrase.toLowerCase()} `) !== -1) {
add_interpretation({ add_interpretation({
match_text: menu_item_phrase, match_text: menu_item_phrase,
exec: ((menu_item)=> ()=> { exec: ((menu_item) => () => {
if (menu_item.checkbox) { if (menu_item.checkbox) {
menu_item.checkbox.toggle(); menu_item.checkbox.toggle();
} else { } else {
@ -1475,12 +1474,12 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
} }
} }
} }
const close_menus_match = input_text.match(/\b(?:(?:close|hide|dismiss) menus?|never ?mind)\b/i); const close_menus_match = input_text.match(/\b(?:(?:close|hide|dismiss) menus?|never ?mind)\b/i);
if (close_menus_match) { if (close_menus_match) {
add_interpretation({ add_interpretation({
match_text: close_menus_match[0], match_text: close_menus_match[0],
exec: ()=> { exec: () => {
// from close_menus in $MenuBar // from close_menus in $MenuBar
$(".menu-button").trigger("release"); $(".menu-button").trigger("release");
// Close any rogue floating submenus // Close any rogue floating submenus
@ -1499,7 +1498,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({ add_interpretation({
match_text: draw_match[0], match_text: draw_match[0],
sketch_subject: subject_matter, sketch_subject: subject_matter,
exec: ()=> { exec: () => {
find_clipart_and_sketch(subject_matter); find_clipart_and_sketch(subject_matter);
}, },
}); });
@ -1507,7 +1506,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
} }
const buttons = $("button, .menu-button, .menu-item-label, label, .help-window .item").filter(":visible").toArray(); const buttons = $("button, .menu-button, .menu-item-label, label, .help-window .item").filter(":visible").toArray();
for (const button of buttons) { for (const button of buttons) {
const button_text = button.textContent || button.getAttribute("aria-label") || button.title; const button_text = button.textContent || button.getAttribute("aria-label") || button.title;
let button_text_phrases = [button_text]; let button_text_phrases = [button_text];
@ -1531,13 +1530,13 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
button_text_phrases = [ button_text_phrases = [
"Toggle Dwell Clicking", "Toggle Dwell Clicks", "Toggle Dwell Clicking", "Toggle Dwell Clicks",
// enable reenable re-enable start resume unpause un-pause // enable reenable re-enable start resume unpause un-pause
"Enable Dwell Clicking", "Enable Eye Gaze", "Enable Gaze Clicking", "Enable Dwell Clicks", "Enable Gaze Clicks", "Enable Dwell Clicking", "Enable Eye Gaze", "Enable Gaze Clicking", "Enable Dwell Clicks", "Enable Gaze Clicks",
"Reenable Dwell Clicking", "Reenable Eye Gaze", "Reenable Gaze Clicking", "Reenable Dwell Clicks", "Reenable Gaze Clicks", "Reenable Dwell Clicking", "Reenable Eye Gaze", "Reenable Gaze Clicking", "Reenable Dwell Clicks", "Reenable Gaze Clicks",
"Re-enable Dwell Clicking", "Re-enable Eye Gaze", "Re-enable Gaze Clicking", "Re-enable Dwell Clicks", "Re-enable Gaze Clicks", "Re-enable Dwell Clicking", "Re-enable Eye Gaze", "Re-enable Gaze Clicking", "Re-enable Dwell Clicks", "Re-enable Gaze Clicks",
"Start Dwell Clicking", "Start Eye Gaze", "Start Gaze Clicking", "Start Dwell Clicks", "Start Gaze Clicks", "Start Dwell Clicking", "Start Eye Gaze", "Start Gaze Clicking", "Start Dwell Clicks", "Start Gaze Clicks",
"Resume Dwell Clicking", "Resume Eye Gaze", "Resume Gaze Clicking", "Resume Dwell Clicks", "Resume Gaze Clicks", "Resume Dwell Clicking", "Resume Eye Gaze", "Resume Gaze Clicking", "Resume Dwell Clicks", "Resume Gaze Clicks",
"Unpause Dwell Clicking", "Unpause Eye Gaze", "Unpause Gaze Clicking", "Unpause Dwell Clicks", "Unpause Gaze Clicks", "Unpause Dwell Clicking", "Unpause Eye Gaze", "Unpause Gaze Clicking", "Unpause Dwell Clicks", "Unpause Gaze Clicks",
"Un-pause Dwell Clicking", "Un-pause Eye Gaze", "Un-pause Gaze Clicking", "Un-pause Dwell Clicks", "Un-pause Gaze Clicks", "Un-pause Dwell Clicking", "Un-pause Eye Gaze", "Un-pause Gaze Clicking", "Un-pause Dwell Clicks", "Un-pause Gaze Clicks",
]; ];
} }
if (button.matches(".window-close-button")) { if (button.matches(".window-close-button")) {
@ -1610,7 +1609,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (button_text === "Color Box") { if (button_text === "Color Box") {
button_text_phrases = ["color box", "color-box", "colorbox"]; button_text_phrases = ["color box", "color-box", "colorbox"];
} }
// top level menu buttons // top level menu buttons
if (button.matches(".menu-button")) { if (button.matches(".menu-button")) {
button_text_phrases = [ button_text_phrases = [
@ -1648,7 +1647,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (` ${input_text} `.toLowerCase().indexOf(` ${match_phrase.toLowerCase()} `) !== -1) { if (` ${input_text} `.toLowerCase().indexOf(` ${match_phrase.toLowerCase()} `) !== -1) {
add_interpretation({ add_interpretation({
match_text: match_phrase, match_text: match_phrase,
exec: ((button)=> ()=> { exec: ((button) => () => {
clickButtonVisibly(button); clickButtonVisibly(button);
console.log("click", button); console.log("click", button);
})(button), })(button),
@ -1666,7 +1665,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({ add_interpretation({
match_text: stop_match[0], match_text: stop_match[0],
type: "stop-drawing", type: "stop-drawing",
exec: ()=> { exec: () => {
window.stopSimulatingGestures && window.stopSimulatingGestures(); window.stopSimulatingGestures && window.stopSimulatingGestures();
window.trace_and_sketch_stop && window.trace_and_sketch_stop(); window.trace_and_sketch_stop && window.trace_and_sketch_stop();
}, },
@ -1710,18 +1709,18 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({ add_interpretation({
match_text: line_width_match[0], match_text: line_width_match[0],
size: n, size: n,
exec: ()=> { exec: () => {
if (isFinite(n)) { if (isFinite(n)) {
// @TODO: DRY with app.js // @TODO: DRY with app.js
if(selected_tool.id === TOOL_BRUSH){ if (selected_tool.id === TOOL_BRUSH) {
brush_size = Math.max(1, Math.min(n, 500)); brush_size = Math.max(1, Math.min(n, 500));
}else if(selected_tool.id === TOOL_ERASER){ } else if (selected_tool.id === TOOL_ERASER) {
eraser_size = Math.max(1, Math.min(n, 500)); eraser_size = Math.max(1, Math.min(n, 500));
}else if(selected_tool.id === TOOL_AIRBRUSH){ } else if (selected_tool.id === TOOL_AIRBRUSH) {
airbrush_size = Math.max(1, Math.min(n, 500)); airbrush_size = Math.max(1, Math.min(n, 500));
}else if(selected_tool.id === TOOL_PENCIL){ } else if (selected_tool.id === TOOL_PENCIL) {
pencil_size = Math.max(1, Math.min(n, 50)); pencil_size = Math.max(1, Math.min(n, 50));
}else if( } else if (
selected_tool.id === TOOL_LINE || selected_tool.id === TOOL_LINE ||
selected_tool.id === TOOL_CURVE || selected_tool.id === TOOL_CURVE ||
selected_tool.id === TOOL_RECTANGLE || selected_tool.id === TOOL_RECTANGLE ||
@ -1731,10 +1730,10 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
) { ) {
stroke_size = Math.max(1, Math.min(n, 500)); stroke_size = Math.max(1, Math.min(n, 500));
} }
$G.trigger("option-changed"); $G.trigger("option-changed");
if(button !== undefined && pointer){ // pointer may only be needed for tests if (button !== undefined && pointer) { // pointer may only be needed for tests
selected_tools.forEach((selected_tool)=> { selected_tools.forEach((selected_tool) => {
tool_go(selected_tool); tool_go(selected_tool);
}); });
} }
@ -1751,7 +1750,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
const scroll_match = input_text.match(scrolling_regexp); const scroll_match = input_text.match(scrolling_regexp);
if (scroll_match) { if (scroll_match) {
const directions = scroll_match[0]; const directions = scroll_match[0];
const vector = {x: 0, y: 0}; const vector = { x: 0, y: 0 };
if (directions.match(/up|north/i)) { if (directions.match(/up|north/i)) {
vector.y = -1; vector.y = -1;
} }
@ -1764,11 +1763,11 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (directions.match(/right|east/i)) { if (directions.match(/right|east/i)) {
vector.x = +1; vector.x = +1;
} }
const scroll_pane_el = $(".window *").toArray().filter((el)=> el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight)[0] || $canvas_area[0]; const scroll_pane_el = $(".window *").toArray().filter((el) => el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight)[0] || $canvas_area[0];
add_interpretation({ add_interpretation({
match_text: scroll_match[0], match_text: scroll_match[0],
exec: ()=> { exec: () => {
const factor = directions.match(/page/) ? 1 : 1/2; const factor = directions.match(/page/) ? 1 : 1 / 2;
// scroll_pane_el.scrollLeft += vector.x * scroll_pane_el.clientWidth * factor; // scroll_pane_el.scrollLeft += vector.x * scroll_pane_el.clientWidth * factor;
// scroll_pane_el.scrollTop += vector.y * scroll_pane_el.clientHeight * factor; // scroll_pane_el.scrollTop += vector.y * scroll_pane_el.clientHeight * factor;
$(scroll_pane_el).animate({ $(scroll_pane_el).animate({
@ -1786,7 +1785,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (new_line_match) { if (new_line_match) {
add_interpretation({ add_interpretation({
match_text: new_line_match[0], match_text: new_line_match[0],
exec: ()=> { exec: () => {
document.execCommand("insertText", false, "\n"); document.execCommand("insertText", false, "\n");
}, },
}); });
@ -1817,7 +1816,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
const text_to_insert = input_text.replace(/new[ -]?line|line[ -]?break|carriage return/g, "\n"); const text_to_insert = input_text.replace(/new[ -]?line|line[ -]?break|carriage return/g, "\n");
add_interpretation({ add_interpretation({
match_text: input_text, match_text: input_text,
exec: ()=> { exec: () => {
if (document.activeElement && document.activeElement.matches("input[type='number']")) { if (document.activeElement && document.activeElement.matches("input[type='number']")) {
document.activeElement.value = input_text; document.activeElement.value = input_text;
} else { } else {
@ -1836,18 +1835,18 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
return interpretations; return interpretations;
}; };
window.trace_and_sketch = (subject_imagedata)=> { window.trace_and_sketch = (subject_imagedata) => {
window.trace_and_sketch_stop && window.trace_and_sketch_stop(); window.trace_and_sketch_stop && window.trace_and_sketch_stop();
// @TODO: clickable cancel button? (in addition to Escape key handling and the "stop" voice command) // @TODO: clickable cancel button? (in addition to Escape key handling and the "stop" voice command)
// I'm suggesting saying "stop drawing" rather than "stop" because I think it's more likely to be picked up as speech at all // I'm suggesting saying "stop drawing" rather than "stop" because I think it's more likely to be picked up as speech at all
$status_text.text(`To stop drawing, ${window.speech_recognition_active ? `say "stop drawing", or ` : ""}press Esc.`); $status_text.text(`To stop drawing, ${window.speech_recognition_active ? `say "stop drawing", or ` : ""}press Esc.`);
// const subject_imagedata = ctx.getImageData(0, 0, canvas.width, canvas.height); // const subject_imagedata = ctx.getImageData(0, 0, canvas.width, canvas.height);
// const pal = palette.map((color)=> get_rgba_from_color(color)).map(([r, g, b, a])=> ({r, g, b, a})); // const pal = palette.map((color)=> get_rgba_from_color(color)).map(([r, g, b, a])=> ({r, g, b, a}));
const trace_data = ImageTracer.imagedataToTracedata(subject_imagedata, { ltres:1, qtres:0.01, scale:10, /*pal,*/ numberofcolors: 6, }); const trace_data = ImageTracer.imagedataToTracedata(subject_imagedata, { ltres: 1, qtres: 0.01, scale: 10, /*pal,*/ numberofcolors: 6, });
const {layers} = trace_data; const { layers } = trace_data;
const brush = get_tool_by_id(TOOL_BRUSH); const brush = get_tool_by_id(TOOL_BRUSH);
select_tool(brush); select_tool(brush);
@ -1855,7 +1854,7 @@ window.trace_and_sketch = (subject_imagedata)=> {
let path_index = 0; let path_index = 0;
let segment_index = 0; let segment_index = 0;
let active_path; let active_path;
window.sketching_iid = setInterval(()=> { window.sketching_iid = setInterval(() => {
const layer = layers[layer_index]; const layer = layers[layer_index];
if (!layer) { if (!layer) {
clearInterval(window.sketching_iid); clearInterval(window.sketching_iid);
@ -1875,15 +1874,15 @@ window.trace_and_sketch = (subject_imagedata)=> {
brush.pointerup(main_ctx, pointer.x, pointer.y); brush.pointerup(main_ctx, pointer.x, pointer.y);
return; return;
} }
let {x1, y1, x2, y2} = segment; let { x1, y1, x2, y2 } = segment;
if (path !== active_path) { if (path !== active_path) {
pointer_previous = {x: x1, y: y1}; pointer_previous = { x: x1, y: y1 };
pointer = {x: x1, y: y1}; pointer = { x: x1, y: y1 };
brush.pointerdown(main_ctx, x1, y1); brush.pointerdown(main_ctx, x1, y1);
active_path = path; active_path = path;
} }
pointer_previous = {x: x1, y: y1}; pointer_previous = { x: x1, y: y1 };
pointer = {x: x2, y: y2}; pointer = { x: x2, y: y2 };
brush.paint(); brush.paint();
pointer_active = true; pointer_active = true;
pointer_over_canvas = true; pointer_over_canvas = true;
@ -1891,15 +1890,15 @@ window.trace_and_sketch = (subject_imagedata)=> {
segment_index += 1; segment_index += 1;
}, 20); }, 20);
}; };
window.trace_and_sketch_stop = ()=> { window.trace_and_sketch_stop = () => {
clearInterval(window.sketching_iid); clearInterval(window.sketching_iid);
pointer_active = false; pointer_active = false;
pointer_over_canvas = false; pointer_over_canvas = false;
}; };
function find_clipart_and_sketch(subject_matter) { function find_clipart_and_sketch(subject_matter) {
find_clipart(subject_matter).then((results)=> { find_clipart(subject_matter).then((results) => {
// @TODO: select less complex images (less file size to width, say?) maybe, and/or better semantic matches by looking for the search terms in the title? // @TODO: select less complex images (less file size to width, say?) maybe, and/or better semantic matches by looking for the search terms in the title?
// detect gradients / spread out histogram at least, and reject based on that // detect gradients / spread out histogram at least, and reject based on that
let image_url = results[~~(Math.random() * results.length)].image_url; let image_url = results[~~(Math.random() * results.length)].image_url;
@ -1909,10 +1908,10 @@ function find_clipart_and_sketch(subject_matter) {
} }
const img = new Image(); const img = new Image();
img.crossOrigin = "Anonymous"; img.crossOrigin = "Anonymous";
img.onerror = ()=> { img.onerror = () => {
$status_text.text("Failed to load clipart."); $status_text.text("Failed to load clipart.");
}; };
img.onload = ()=> { img.onload = () => {
// @TODO: find an empty spot on the canvas for the sketch, smaller if need be // @TODO: find an empty spot on the canvas for the sketch, smaller if need be
const max_sketch_width = 500; const max_sketch_width = 500;
const max_sketch_height = 500; const max_sketch_height = 500;
@ -1932,7 +1931,7 @@ function find_clipart_and_sketch(subject_matter) {
trace_and_sketch(image_data); trace_and_sketch(image_data);
}; };
img.src = image_url; img.src = image_url;
}, (error)=> { }, (error) => {
if (error.code === "no-results") { if (error.code === "no-results") {
$status_text.text(`No clipart found for '${subject_matter}'`); $status_text.text(`No clipart found for '${subject_matter}'`);
} else { } else {
@ -1944,12 +1943,12 @@ function find_clipart_and_sketch(subject_matter) {
function find_clipart(query) { function find_clipart(query) {
const bing_url = new URL(`https://www.bing.com/images/search?q=${encodeURIComponent(query)}&qft=+filterui:photo-clipart&FORM=IRFLTR`) const bing_url = new URL(`https://www.bing.com/images/search?q=${encodeURIComponent(query)}&qft=+filterui:photo-clipart&FORM=IRFLTR`)
return fetch(`https://jspaint-cors-proxy.herokuapp.com/${bing_url}`) return fetch(`https://jspaint-cors-proxy.herokuapp.com/${bing_url}`)
.then(response=> response.text()) .then(response => response.text())
.then((html)=> { .then((html) => {
// handle relative data-src // handle relative data-src
html = html.replace( html = html.replace(
/((?:data-src)=["'])(?!(?:https?:|data:))(\/?)/gi, /((?:data-src)=["'])(?!(?:https?:|data:))(\/?)/gi,
($0, $1, $2)=> `${$1}${bing_url.origin}${$2 ? bing_url.pathname : ""}` ($0, $1, $2) => `${$1}${bing_url.origin}${$2 ? bing_url.pathname : ""}`
); );
// handle relative src and href in a less error-prone way, with a <base> tag // handle relative src and href in a less error-prone way, with a <base> tag
const doc = new DOMParser().parseFromString(html, "text/html"); const doc = new DOMParser().parseFromString(html, "text/html");
@ -1962,34 +1961,34 @@ function find_clipart(query) {
window.search_page_$html = $html; window.search_page_$html = $html;
console.log("window.search_page_html and window.search_page_$html are a available for debugging"); console.log("window.search_page_html and window.search_page_$html are a available for debugging");
const validate_item = (item)=> item.image_url && (item.image_url.match(/^data:/) ? item.image_url.length > 1000 : true); const validate_item = (item) => item.image_url && (item.image_url.match(/^data:/) ? item.image_url.length > 1000 : true);
let items = $html.find("[m]").toArray() let items = $html.find("[m]").toArray()
.map((el)=> el.getAttribute("m")) .map((el) => el.getAttribute("m"))
.map((json)=> { .map((json) => {
try { try {
return JSON.parse(json); return JSON.parse(json);
} catch(error) { } catch (error) {
return null; return null;
} }
}) })
.filter((maybe_parsed)=> maybe_parsed && maybe_parsed.murl) .filter((maybe_parsed) => maybe_parsed && maybe_parsed.murl)
.map(({murl, t})=> ({image_url: murl, title: t || ""})) .map(({ murl, t }) => ({ image_url: murl, title: t || "" }))
.filter(validate_item); .filter(validate_item);
// fallback to thumbnails in case they get rid of the "m" attribute (thumbnails are not as good, more likely to be jpeg) // fallback to thumbnails in case they get rid of the "m" attribute (thumbnails are not as good, more likely to be jpeg)
if (items.length === 0) { if (items.length === 0) {
console.log("Fallback to thumbnails"); console.log("Fallback to thumbnails");
items = $html.find("img.mimg").toArray() items = $html.find("img.mimg").toArray()
.map((el)=> ({image_url: el.src || el.dataset.src, title: ""})) .map((el) => ({ image_url: el.src || el.dataset.src, title: "" }))
.filter(validate_item); .filter(validate_item);
} }
// fallback in case they also change the class for images (this may match totally irrelevant things) // fallback in case they also change the class for images (this may match totally irrelevant things)
if (items.length === 0) { if (items.length === 0) {
console.log("Fallback to most imgs"); console.log("Fallback to most imgs");
items = $html.find("img:not(.sw_spd):not(.rms_img):not(.flagIcon)").toArray() items = $html.find("img:not(.sw_spd):not(.rms_img):not(.flagIcon)").toArray()
.filter((el)=> !el.closest("[role='navigation'], nav")) // ignore "Related searches", "Refine your search" etc. .filter((el) => !el.closest("[role='navigation'], nav")) // ignore "Related searches", "Refine your search" etc.
.map((el)=> ({image_url: el.src || el.dataset.src, title: ""})) .map((el) => ({ image_url: el.src || el.dataset.src, title: "" }))
.filter(validate_item); .filter(validate_item);
} }
console.log(`Search results for '${query}':`, items); console.log(`Search results for '${query}':`, items);
@ -2021,7 +2020,7 @@ function clickButtonVisibly(button) {
if (button.matches("button:not(.toggle)")) { if (button.matches("button:not(.toggle)")) {
button.style.borderImage = "var(--inset-deep-border-image)"; button.style.borderImage = "var(--inset-deep-border-image)";
setTimeout(()=> { setTimeout(() => {
button.style.borderImage = ""; button.style.borderImage = "";
// delay the button.click() as well, so the pressed state is // delay the button.click() as well, so the pressed state is
// visible even if the button action closes a dialog // visible even if the button action closes a dialog
@ -2056,9 +2055,9 @@ Expected '${input_text}' to be interpreted as`, expected, `but found no interpre
return; return;
} }
const interpretation = choose_interpretation(interpretations); const interpretation = choose_interpretation(interpretations);
const actual = Object.assign({}, interpretation, {prioritize: undefined, exec: undefined}); const actual = Object.assign({}, interpretation, { prioritize: undefined, exec: undefined });
// expected.match_text = expected.match_text || input_text; // puts key in wrong order // expected.match_text = expected.match_text || input_text; // puts key in wrong order
expected = Object.assign({match_text: input_text}, expected); expected = Object.assign({ match_text: input_text }, expected);
const expected_json = JSON.stringify(expected, null, 4); const expected_json = JSON.stringify(expected, null, 4);
const actual_json = JSON.stringify(actual, null, 4); const actual_json = JSON.stringify(actual, null, 4);
if (expected_json !== actual_json) { if (expected_json !== actual_json) {
@ -2073,8 +2072,8 @@ All interpretations:`, interpretations);
const fixed_up_input_text = fix_up_speech_recognition(input_text); const fixed_up_input_text = fix_up_speech_recognition(input_text);
if (fixed_up_input_text !== input_text) { if (fixed_up_input_text !== input_text) {
console.error(`Failed test. Speech recognition fixup changed the input from: console.error(`Failed test. Speech recognition fixup changed the input from:
'${input_text}' to: '${input_text}' to:
'${fixed_up_input_text}'`); '${fixed_up_input_text}'`);
return; return;
} }
} }
@ -2085,9 +2084,9 @@ function test_speech(input_text, expected) {
if (typeof expected === "string") { if (typeof expected === "string") {
if (fixed_up_input_text !== expected) { if (fixed_up_input_text !== expected) {
console.error(`Failed test. Speech recognition fixup changed the input from: console.error(`Failed test. Speech recognition fixup changed the input from:
'${input_text}' to: '${input_text}' to:
'${fixed_up_input_text}' instead of: '${fixed_up_input_text}' instead of:
'${expected}'`); '${expected}'`);
return; return;
} }
} else { } else {
@ -2097,40 +2096,40 @@ function test_speech(input_text, expected) {
function test_speech_recognition() { function test_speech_recognition() {
// test_command("select blue", {color: "blue"}); // @FIXME // test_command("select blue", {color: "blue"}); // @FIXME
test_command("select fill", {tool_id: TOOL_FILL}); test_command("select fill", { tool_id: TOOL_FILL });
test_command("select text", {tool_id: TOOL_TEXT}); test_command("select text", { tool_id: TOOL_TEXT });
test_command("select", {tool_id: TOOL_SELECT}); test_command("select", { tool_id: TOOL_SELECT });
test_speech("free form select", {tool_id: TOOL_FREE_FORM_SELECT}); test_speech("free form select", { tool_id: TOOL_FREE_FORM_SELECT });
test_speech("lips", {match_text: "ellipse", tool_id: TOOL_ELLIPSE}); test_speech("lips", { match_text: "ellipse", tool_id: TOOL_ELLIPSE });
test_command("", null); test_command("", null);
// test_command("I got you some new books", null); // test_command("I got you some new books", null);
// test_command("pan view sorthweast", null); // currently opens View menu // test_command("pan view sorthweast", null); // currently opens View menu
test_command("1 pixel lines", {size: 1}); test_command("1 pixel lines", { size: 1 });
test_command("1 pixel wide lines", {size: 1}); test_command("1 pixel wide lines", { size: 1 });
test_command("set line width to 5", {size: 5}); test_command("set line width to 5", { size: 5 });
// test_command("use medium-small stroke size", {match_text: "use medium-small stroke size", size: NaN}); // test_command("use medium-small stroke size", {match_text: "use medium-small stroke size", size: NaN});
test_speech("set line lips to a hundred", {match_text: "set line width to a hundred", size: 100}); test_speech("set line lips to a hundred", { match_text: "set line width to a hundred", size: 100 });
test_command("use stroke size 10 pixels", {size: 10}); test_command("use stroke size 10 pixels", { size: 10 });
// test_command("use stroke size of 10 pixels", {match_text: "use stroke size of 10 pixels", size: 10}); // test_command("use stroke size of 10 pixels", {match_text: "use stroke size of 10 pixels", size: 10});
test_command("draw a :-)", {sketch_subject: "smiley face"}); test_command("draw a :-)", { sketch_subject: "smiley face" });
// test_command("draw sample text", {sketch_subject: "sample text"}); // @FIXME // test_command("draw sample text", {sketch_subject: "sample text"}); // @FIXME
test_command("end", {type: "stop-drawing"}); test_command("end", { type: "stop-drawing" });
test_command("stop", {type: "stop-drawing"}); test_command("stop", { type: "stop-drawing" });
test_command("draw a stop sign", {sketch_subject: "stop sign"}); test_command("draw a stop sign", { sketch_subject: "stop sign" });
test_command("pan view southwest", {vector: {x: -1, y: +1}}); test_command("pan view southwest", { vector: { x: -1, y: +1 } });
test_command("pan southeast", {vector: {x: +1, y: +1}}); test_command("pan southeast", { vector: { x: +1, y: +1 } });
test_command("move view northwest", {vector: {x: -1, y: -1}}); test_command("move view northwest", { vector: { x: -1, y: -1 } });
test_command("view northwest", {vector: {x: -1, y: -1}}); test_command("view northwest", { vector: { x: -1, y: -1 } });
test_command("move viewport northwest", {vector: {x: -1, y: -1}}); test_command("move viewport northwest", { vector: { x: -1, y: -1 } });
test_command("pan down", {vector: {x: 0, y: +1}}); test_command("pan down", { vector: { x: 0, y: +1 } });
test_command("scroll down", {vector: {x: 0, y: +1}}); test_command("scroll down", { vector: { x: 0, y: +1 } });
test_command("go downwards", {vector: {x: 0, y: +1}}); test_command("go downwards", { vector: { x: 0, y: +1 } });
test_command("go upward", {vector: {x: 0, y: -1}}); test_command("go upward", { vector: { x: 0, y: -1 } });
test_command("go downwards and to the left", {vector: {x: -1, y: +1}}); test_command("go downwards and to the left", { vector: { x: -1, y: +1 } });
test_command("go up to the left", {vector: {x: -1, y: -1}}); test_command("go up to the left", { vector: { x: -1, y: -1 } });
test_speech("cool up", {match_text: "go up", vector: {x: 0, y: -1}}); test_speech("cool up", { match_text: "go up", vector: { x: 0, y: -1 } });
test_command("scroll the view southward", {vector: {x: 0, y: +1}}); test_command("scroll the view southward", { vector: { x: 0, y: +1 } });
} }

View File

@ -3,7 +3,7 @@
const theme_storage_key = "jspaint theme"; const theme_storage_key = "jspaint theme";
const disable_seasonal_theme_key = "jspaint disable seasonal theme"; const disable_seasonal_theme_key = "jspaint disable seasonal theme";
const href_for = theme => `styles/themes/${theme}`; const href_for = theme => `styles/themes/${theme}`;
let iid; let iid;
function wait_for_theme_loaded(theme, callback) { function wait_for_theme_loaded(theme, callback) {
clearInterval(iid); clearInterval(iid);
@ -55,10 +55,10 @@
localStorage[theme_storage_key] = theme; localStorage[theme_storage_key] = theme;
localStorage[disable_seasonal_theme_key] = "true"; // any theme change disables seasonal theme (unless of course you select the seasonal theme) localStorage[disable_seasonal_theme_key] = "true"; // any theme change disables seasonal theme (unless of course you select the seasonal theme)
grinch_button?.remove(); grinch_button?.remove();
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch(error) {} } catch (error) { }
const signal_theme_load = ()=> { const signal_theme_load = () => {
$(window).triggerHandler("theme-load"); $(window).triggerHandler("theme-load");
$(window).trigger("resize"); // not exactly, but get dynamic cursor to update its offset $(window).trigger("resize"); // not exactly, but get dynamic cursor to update its offset
}; };

View File

@ -15,9 +15,9 @@ const ChooserCanvas = (
) => { ) => {
const c = reuse_canvas(width, height); const c = reuse_canvas(width, height);
let img = ChooserCanvas.cache[url]; let img = ChooserCanvas.cache[url];
if(!img){ if (!img) {
img = ChooserCanvas.cache[url] = E("img"); img = ChooserCanvas.cache[url] = E("img");
img.onerror = ()=> { img.onerror = () => {
delete ChooserCanvas.cache[url]; delete ChooserCanvas.cache[url];
}; };
img.src = url; img.src = url;
@ -29,8 +29,8 @@ const ChooserCanvas = (
sourceX, sourceY, sourceWidth, sourceHeight, sourceX, sourceY, sourceWidth, sourceHeight,
destX, destY, destWidth, destHeight destX, destY, destWidth, destHeight
); );
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch(error) {} } catch (error) { }
// if(invert){ // if(invert){
// invert_rgb(c.ctx); // can fail due to tainted canvas if running from file: protocol // invert_rgb(c.ctx); // can fail due to tainted canvas if running from file: protocol
// } // }
@ -62,7 +62,7 @@ const ChooserDiv = (
div.classList.add(class_name); div.classList.add(class_name);
div.style.width = sourceWidth + "px"; div.style.width = sourceWidth + "px";
div.style.height = sourceHeight + "px"; div.style.height = sourceHeight + "px";
// @TODO: single listener for all divs // @TODO: single listener for all divs
const on_zoom_etc = () => { const on_zoom_etc = () => {
const use_svg = (window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0); const use_svg = (window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0);
@ -74,7 +74,7 @@ const ChooserDiv = (
$G.on("theme-load resize", on_zoom_etc); $G.on("theme-load resize", on_zoom_etc);
div._on_zoom_etc = on_zoom_etc; div._on_zoom_etc = on_zoom_etc;
on_zoom_etc(); on_zoom_etc();
div.style.backgroundPosition = `${-sourceX}px ${-sourceY}px`; div.style.backgroundPosition = `${-sourceX}px ${-sourceY}px`;
div.style.borderColor = "transparent"; div.style.borderColor = "transparent";
div.style.borderStyle = "solid"; div.style.borderStyle = "solid";
@ -92,7 +92,7 @@ const ChooserDiv = (
const $Choose = (things, display, choose, is_chosen, gray_background_for_unselected) => { const $Choose = (things, display, choose, is_chosen, gray_background_for_unselected) => {
const $chooser = $(E("div")).addClass("chooser").css("touch-action", "none"); const $chooser = $(E("div")).addClass("chooser").css("touch-action", "none");
const choose_thing = (thing) => { const choose_thing = (thing) => {
if(is_chosen(thing)){ if (is_chosen(thing)) {
return; return;
} }
choose(thing); choose(thing);
@ -103,11 +103,11 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
return; return;
} }
$chooser.empty(); $chooser.empty();
for(let i=0; i<things.length; i++){ for (let i = 0; i < things.length; i++) {
(thing => { (thing => {
const $option_container = $(E("div")).addClass("chooser-option").appendTo($chooser); const $option_container = $(E("div")).addClass("chooser-option").appendTo($chooser);
$option_container.data("thing", thing); $option_container.data("thing", thing);
const reuse_canvas = (width, height)=> { const reuse_canvas = (width, height) => {
let option_canvas = $option_container.find("canvas")[0]; let option_canvas = $option_container.find("canvas")[0];
if (option_canvas) { if (option_canvas) {
if (option_canvas.width !== width) { option_canvas.width = width; } if (option_canvas.width !== width) { option_canvas.width = width; }
@ -144,13 +144,13 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
})(things[i]); })(things[i]);
} }
const onpointerover_while_pointer_down = (event)=> { const onpointerover_while_pointer_down = (event) => {
const option_container = event.target.closest(".chooser-option"); const option_container = event.target.closest(".chooser-option");
if (option_container) { if (option_container) {
choose_thing($(option_container).data("thing")); choose_thing($(option_container).data("thing"));
} }
}; };
const ontouchmove_while_pointer_down = (event)=> { const ontouchmove_while_pointer_down = (event) => {
const touch = event.originalEvent.changedTouches[0]; const touch = event.originalEvent.changedTouches[0];
const target = document.elementFromPoint(touch.clientX, touch.clientY); const target = document.elementFromPoint(touch.clientX, touch.clientY);
const option_container = target.closest(".chooser-option"); const option_container = target.closest(".chooser-option");
@ -159,7 +159,7 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
} }
event.preventDefault(); event.preventDefault();
}; };
$chooser.on("pointerdown click", (event)=> { $chooser.on("pointerdown click", (event) => {
const option_container = event.target.closest(".chooser-option"); const option_container = event.target.closest(".chooser-option");
if (option_container) { if (option_container) {
choose_thing($(option_container).data("thing")); choose_thing($(option_container).data("thing"));
@ -170,7 +170,7 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
$chooser.on("touchmove", ontouchmove_while_pointer_down); $chooser.on("touchmove", ontouchmove_while_pointer_down);
} }
}); });
$G.on("pointerup pointercancel", ()=> { $G.on("pointerup pointercancel", () => {
$chooser.off("pointerover", onpointerover_while_pointer_down); $chooser.off("pointerover", onpointerover_while_pointer_down);
$chooser.off("touchmove", ontouchmove_while_pointer_down); $chooser.off("touchmove", ontouchmove_while_pointer_down);
}); });
@ -180,48 +180,48 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
const $ChooseShapeStyle = () => { const $ChooseShapeStyle = () => {
const $chooser = $Choose( const $chooser = $Choose(
[ [
{stroke: true, fill: false}, { stroke: true, fill: false },
{stroke: true, fill: true}, { stroke: true, fill: true },
{stroke: false, fill: true} { stroke: false, fill: true }
], ],
({stroke, fill}, is_chosen, reuse_canvas) => { ({ stroke, fill }, is_chosen, reuse_canvas) => {
const ss_canvas = reuse_canvas(39, 21); const ss_canvas = reuse_canvas(39, 21);
const ss_ctx = ss_canvas.ctx; const ss_ctx = ss_canvas.ctx;
// border px inwards amount // border px inwards amount
let b = 5; let b = 5;
const style = getComputedStyle(ss_canvas); const style = getComputedStyle(ss_canvas);
ss_ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText"); ss_ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
if(stroke){ if (stroke) {
// just using a solid rectangle for the stroke // just using a solid rectangle for the stroke
// so as not to have to deal with the pixel grid with strokes // so as not to have to deal with the pixel grid with strokes
ss_ctx.fillRect(b, b, ss_canvas.width-b*2, ss_canvas.height-b*2); ss_ctx.fillRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
} }
// go inward a pixel for the fill // go inward a pixel for the fill
b += 1; b += 1;
ss_ctx.fillStyle = style.getPropertyValue("--ButtonShadow"); ss_ctx.fillStyle = style.getPropertyValue("--ButtonShadow");
if(fill){ if (fill) {
ss_ctx.fillRect(b, b, ss_canvas.width-b*2, ss_canvas.height-b*2); ss_ctx.fillRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
}else{ } else {
ss_ctx.clearRect(b, b, ss_canvas.width-b*2, ss_canvas.height-b*2); ss_ctx.clearRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
} }
return ss_canvas; return ss_canvas;
}, },
({stroke, fill}) => { ({ stroke, fill }) => {
$chooser.stroke = stroke; $chooser.stroke = stroke;
$chooser.fill = fill; $chooser.fill = fill;
}, },
({stroke, fill}) => $chooser.stroke === stroke && $chooser.fill === fill ({ stroke, fill }) => $chooser.stroke === stroke && $chooser.fill === fill
).addClass("choose-shape-style"); ).addClass("choose-shape-style");
$chooser.fill = false; $chooser.fill = false;
$chooser.stroke = true; $chooser.stroke = true;
return $chooser; return $chooser;
}; };
@ -231,9 +231,9 @@ const $choose_brush = $Choose(
const circular_brush_sizes = [7, 4, 1]; const circular_brush_sizes = [7, 4, 1];
const brush_sizes = [8, 5, 2]; const brush_sizes = [8, 5, 2];
const things = []; const things = [];
brush_shapes.forEach((brush_shape)=> { brush_shapes.forEach((brush_shape) => {
const sizes = brush_shape === "circle" ? circular_brush_sizes : brush_sizes; const sizes = brush_shape === "circle" ? circular_brush_sizes : brush_sizes;
sizes.forEach((brush_size)=> { sizes.forEach((brush_size) => {
things.push({ things.push({
shape: brush_shape, shape: brush_shape,
size: brush_size, size: brush_size,
@ -245,30 +245,30 @@ const $choose_brush = $Choose(
(o, is_chosen, reuse_canvas) => { (o, is_chosen, reuse_canvas) => {
const cb_canvas = reuse_canvas(10, 10); const cb_canvas = reuse_canvas(10, 10);
const style = getComputedStyle(cb_canvas); const style = getComputedStyle(cb_canvas);
const shape = o.shape; const shape = o.shape;
const size = o.size; const size = o.size;
const color = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText"); const color = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
stamp_brush_canvas(cb_canvas.ctx, 5, 5, shape, size); stamp_brush_canvas(cb_canvas.ctx, 5, 5, shape, size);
replace_colors_with_swatch(cb_canvas.ctx, color); replace_colors_with_swatch(cb_canvas.ctx, color);
return cb_canvas; return cb_canvas;
}, ({shape, size}) => { }, ({ shape, size }) => {
brush_shape = shape; brush_shape = shape;
brush_size = size; brush_size = size;
}, ({shape, size}) => brush_shape === shape && brush_size === size }, ({ shape, size }) => brush_shape === shape && brush_size === size
).addClass("choose-brush"); ).addClass("choose-brush");
const $choose_eraser_size = $Choose( const $choose_eraser_size = $Choose(
[4, 6, 8, 10], [4, 6, 8, 10],
(size, is_chosen, reuse_canvas) => { (size, is_chosen, reuse_canvas) => {
const ce_canvas = reuse_canvas(39, 16); const ce_canvas = reuse_canvas(39, 16);
const style = getComputedStyle(ce_canvas); const style = getComputedStyle(ce_canvas);
ce_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText"); ce_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
render_brush(ce_canvas.ctx, "square", size); render_brush(ce_canvas.ctx, "square", size);
return ce_canvas; return ce_canvas;
}, },
size => { size => {
@ -285,7 +285,7 @@ const $choose_stroke_size = $Choose(
const center_y = (h - size) / 2; const center_y = (h - size) / 2;
const style = getComputedStyle(cs_canvas); const style = getComputedStyle(cs_canvas);
cs_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText"); cs_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
cs_canvas.ctx.fillRect(b, ~~center_y, w - b*2, size); cs_canvas.ctx.fillRect(b, ~~center_y, w - b * 2, size);
return cs_canvas; return cs_canvas;
}, },
size => { size => {
@ -304,11 +304,11 @@ const $choose_magnification = $Choose(
"magnification-option", "magnification-option",
is_chosen, // invert if chosen is_chosen, // invert if chosen
39, (secret ? 2 : 13), // width, height of destination canvas 39, (secret ? 2 : 13), // width, height of destination canvas
i*23, 0, 23, 9, // x, y, width, height from source image i * 23, 0, 23, 9, // x, y, width, height from source image
8, 2, 23, 9, // x, y, width, height on destination 8, 2, 23, 9, // x, y, width, height on destination
reuse_div, reuse_div,
); );
if(secret){ if (secret) {
$(chooser_el).addClass("secret-option"); $(chooser_el).addClass("secret-option");
} }
return chooser_el; return chooser_el;
@ -319,30 +319,30 @@ const $choose_magnification = $Choose(
scale => scale === magnification, scale => scale === magnification,
true, true,
).addClass("choose-magnification") ).addClass("choose-magnification")
.css({position: "relative"}); // positioning context for above `position: "absolute"` canvas .css({ position: "relative" }); // positioning context for above `position: "absolute"` canvas
$choose_magnification.on("update", () => { $choose_magnification.on("update", () => {
$choose_magnification $choose_magnification
.find(".secret-option") .find(".secret-option")
.parent() .parent()
.css({position: "absolute", bottom: "-2px", left: 0, opacity: 0, height: 2, overflow: "hidden" }); .css({ position: "absolute", bottom: "-2px", left: 0, opacity: 0, height: 2, overflow: "hidden" });
}); });
const airbrush_sizes = [9, 16, 24]; const airbrush_sizes = [9, 16, 24];
const $choose_airbrush_size = $Choose( const $choose_airbrush_size = $Choose(
airbrush_sizes, airbrush_sizes,
(size, is_chosen, reuse_canvas) => { (size, is_chosen, reuse_canvas) => {
const image_width = 72; // width of source image const image_width = 72; // width of source image
const i = airbrush_sizes.indexOf(size); // 0 or 1 or 2 const i = airbrush_sizes.indexOf(size); // 0 or 1 or 2
const l = airbrush_sizes.length; // 3 const l = airbrush_sizes.length; // 3
const is_bottom = (i === 2); const is_bottom = (i === 2);
const shrink = 4 * !is_bottom; const shrink = 4 * !is_bottom;
const w = image_width / l - shrink * 2; const w = image_width / l - shrink * 2;
const h = 23; const h = 23;
const source_x = image_width / l * i + shrink; const source_x = image_width / l * i + shrink;
return ChooserCanvas( return ChooserCanvas(
"images/options-airbrush-size.png", "images/options-airbrush-size.png",
is_chosen, // invert if chosen is_chosen, // invert if chosen
@ -368,7 +368,7 @@ const $choose_transparent_mode = $Choose(
return ChooserDiv( return ChooserDiv(
"transparent-mode-option", "transparent-mode-option",
false, // never invert it false, // never invert it
b+sw+b, b+sh+b, // width, height of created destination canvas b + sw + b, b + sh + b, // width, height of created destination canvas
0, option ? 22 : 0, sw, sh, // x, y, width, height from source image 0, option ? 22 : 0, sw, sh, // x, y, width, height from source image
b, b, sw, sh, // x, y, width, height on created destination canvas b, b, sw, sh, // x, y, width, height on created destination canvas
reuse_div, reuse_div,

File diff suppressed because it is too large Load Diff

View File

@ -30,8 +30,8 @@
cancelAnimationFrame(rAF_ID); cancelAnimationFrame(rAF_ID);
$(rotologo).remove(); $(rotologo).remove();
$window.css({transform: ""}); $window.css({ transform: "" });
removeEventListener("keydown", space_phase_key_handler); removeEventListener("keydown", space_phase_key_handler);
if (player) { if (player) {
player.destroy(); player.destroy();
@ -71,18 +71,15 @@
$(rotologo).css({ $(rotologo).css({
transform: transform:
`perspective(4000px) rotateY(${ `perspective(4000px) rotateY(${Math.sin(Date.now() / 5000)
Math.sin(Date.now() / 5000) }turn) rotateX(${0
}turn) rotateX(${
0
}turn) translate(-50%, -50%) translateZ(500px)`, }turn) translate(-50%, -50%) translateZ(500px)`,
filter: filter:
`hue-rotate(${ `hue-rotate(${Math.sin(Date.now() / 4000)
Math.sin(Date.now() / 4000) // @TODO: slow down and stop when you pause
// @TODO: slow down and stop when you pause
}turn)`, }turn)`,
}); });
if ($window.length) { if ($window.length) {
let el = $window[0]; let el = $window[0];
let offsetLeft = 0; let offsetLeft = 0;
@ -95,10 +92,8 @@
$window.css({ $window.css({
transform: transform:
`perspective(4000px) rotateY(${ `perspective(4000px) rotateY(${-(offsetLeft + ($window.outerWidth() - parent.innerWidth) / 2) / parent.innerWidth / 3
-(offsetLeft + ($window.outerWidth() - parent.innerWidth) / 2) / parent.innerWidth / 3 }turn) rotateX(${(offsetTop + ($window.outerHeight() - parent.innerHeight) / 2) / parent.innerHeight / 3
}turn) rotateX(${
(offsetTop + ($window.outerHeight() - parent.innerHeight) / 2) / parent.innerHeight / 3
}turn)`, }turn)`,
transformOrigin: "50% 50%", transformOrigin: "50% 50%",
transformStyle: "preserve-3d", transformStyle: "preserve-3d",
@ -156,7 +151,7 @@
if (event.data == YT.PlayerState.PLAYING) { if (event.data == YT.PlayerState.PLAYING) {
// @TODO: pause and resume this timer with the video // @TODO: pause and resume this timer with the video
setTimeout(() => { setTimeout(() => {
$(rotologo).css({opacity: 1}); $(rotologo).css({ opacity: 1 });
}, 14150); }, 14150);
} }
if (event.data == YT.PlayerState.ENDED) { if (event.data == YT.PlayerState.ENDED) {
@ -167,7 +162,7 @@
// setTimeout/setInterval and check player.getCurrentTime() for when near the end? // setTimeout/setInterval and check player.getCurrentTime() for when near the end?
// or we might switch to using soundcloud for the audio and so trigger it with that, with a separate video of just clouds // or we might switch to using soundcloud for the audio and so trigger it with that, with a separate video of just clouds
// also fade out the rotologo earlier // also fade out the rotologo earlier
$(rotologo).css({opacity: 0}); $(rotologo).css({ opacity: 0 });
// destroy rotologo once faded out // destroy rotologo once faded out
setTimeout(stop_vaporwave, 1200); setTimeout(stop_vaporwave, 1200);
} }
@ -182,14 +177,14 @@
player.pauseVideo(); player.pauseVideo();
is_theoretically_playing = false; is_theoretically_playing = false;
$(player.getIframe()) $(player.getIframe())
.add(rotologo) .add(rotologo)
.css({opacity: "0"}); .css({ opacity: "0" });
} else { } else {
player.playVideo(); player.playVideo();
is_theoretically_playing = true; is_theoretically_playing = true;
$(player.getIframe()) $(player.getIframe())
.add(rotologo) .add(rotologo)
.css({opacity: ""}); .css({ opacity: "" });
} }
e.preventDefault(); e.preventDefault();
// player.getIframe().focus(); // player.getIframe().focus();
@ -207,7 +202,7 @@
}; };
addEventListener("keydown", Konami.code(toggle_vaporwave)); addEventListener("keydown", Konami.code(toggle_vaporwave));
addEventListener("keydown", (event)=> { addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
stop_vaporwave(); stop_vaporwave();
} }

View File

@ -1,4 +1,3 @@
<div id="news" hidden> <div id="news" hidden>
<article class="news-entry" id="news-3000-aliens"> <article class="news-entry" id="news-3000-aliens">
<h1>Extraterrestrial Life</h1> <h1>Extraterrestrial Life</h1>
@ -17,6 +16,7 @@
background: black; background: black;
color: aqua; color: aqua;
} }
.news-entry { .news-entry {
background: rgba(0, 255, 255, 0.2); background: rgba(0, 255, 255, 0.2);
color: aqua; color: aqua;
@ -26,15 +26,18 @@
max-width: 30em; max-width: 30em;
margin: 10px; margin: 10px;
} }
.news-entry > h1 {
.news-entry>h1 {
font-size: 1.3em; font-size: 1.3em;
margin: 0; margin: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.news-entry > time {
.news-entry>time {
font-size: 1.3em; font-size: 1.3em;
color: rgba(0, 255, 255, 0.5); color: rgba(0, 255, 255, 0.5);
} }
.news-entry .new { .news-entry .new {
color: lime; color: lime;
} }