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

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

View File

@ -18,7 +18,7 @@ context('tool tests', () => {
});
beforeEach(() => {
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.background = "#fff";
win.brush_shape = win.default_brush_shape;
@ -31,7 +31,7 @@ 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];
let startWithinRect = target.getBoundingClientRect();
let canvasAreaRect = win.$(".canvas-area")[0].getBoundingClientRect();
@ -99,7 +99,7 @@ context('tool tests', () => {
};
};
return new Promise((resolve)=> {
return new Promise((resolve) => {
triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor
triggerMouseEvent("pointerdown", pointForTime(t));
let move = () => {
@ -160,30 +160,30 @@ context('tool tests', () => {
it(`eraser tool`, () => {
cy.get(`.tool[title='Eraser/Color Eraser']`).click();
// gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
cy.window().then({timeout: 60000}, async (win)=> {
for (let row=0; row<4; row++) {
cy.window().then({ timeout: 60000 }, async (win) => {
for (let row = 0; row < 4; row++) {
const secondary = !!(row % 2);
const increaseSize = row >= 2;
let $options = win.$(`.chooser > *`);
for (let o=0; o<$options.length; o++) {
for (let o = 0; o < $options.length; o++) {
$options[o].click();
if (increaseSize) {
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";
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};
await simulateGesture(win, {shift: false, secondary: false, start, end});
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 };
await simulateGesture(win, { shift: false, secondary: false, start, end });
if (secondary) {
// eslint-disable-next-line require-atomic-updates
win.selected_colors.background = "#ff0";
// eslint-disable-next-line require-atomic-updates
win.selected_colors.foreground = "#f0f";
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};
await simulateGesture(win, {shift: false, secondary: true, start, end});
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 };
await simulateGesture(win, { shift: false, secondary: true, start, end });
}
}
}
@ -191,13 +191,13 @@ context('tool tests', () => {
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`, () => {
cy.get(`.tool[title='${toolName}']`).click();
// gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
cy.get(".swatch:nth-child(22)").rightclick();
cy.window().then({timeout: 60000}, async (win)=> {
for (let row=0; row<4; row++) {
cy.window().then({ timeout: 60000 }, async (win) => {
for (let row = 0; row < 4; row++) {
const secondary = !!(row % 2);
const increaseSize = row >= 2;
let $options = win.$(`.chooser > *`);
@ -205,16 +205,16 @@ context('tool tests', () => {
if ($options.length === 0) {
$options = win.$("<dummy>");
}
for (let o=0; o<$options.length; o++) {
for (let o = 0; o < $options.length; o++) {
$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++) {
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 end = {x: start.x + 0.04, y: start.y + 0.04};
await simulateGesture(win, {shift: false, secondary: !!secondary, start, end});
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 };
await simulateGesture(win, { shift: false, secondary: !!secondary, start, end });
}
}
});

View File

@ -59,7 +59,7 @@ context('visual tests', () => {
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
});
beforeEach(()=> {
beforeEach(() => {
if (Cypress.$('.window:visible')[0]) {
cy.get('.window:visible .window-close-button').click();
cy.get('.window').should('not.be.visible');
@ -90,7 +90,7 @@ context('visual tests', () => {
// @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-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
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions, { blackout: ["iframe"] }));
});
@ -133,7 +133,7 @@ context('visual tests', () => {
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-item", "Edit Colors").click();
cy.wait(100);
@ -169,7 +169,7 @@ context('visual tests', () => {
cy.matchImageSnapshot(withTextCompareOptions);
});
it('classic theme edit colors dialog', ()=> {
it('classic theme edit colors dialog', () => {
test_edit_colors_dialog(false);
});

View File

@ -1,6 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<title>JS Paint</title>
@ -34,7 +35,8 @@
This is not an @import in classic.css because it needs RTLCSS and I'm not applying RTLCSS to themes yet.
So I added .not-for-modern logic to theme.js to exclude these styles depending on the theme.
-->
<link href="lib/98.css/98.custom-build.css" class="flippable-layout-stylesheet not-for-modern" rel="stylesheet" type="text/css">
<link href="lib/98.css/98.custom-build.css" class="flippable-layout-stylesheet not-for-modern" rel="stylesheet"
type="text/css">
<link rel="apple-touch-icon" href="images/icons/apple-icon-180x180.png">
<!-- Chrome will pick the largest image for some reason, instead of the most appropriate one. -->
@ -67,11 +69,12 @@
<script src="src/error-handling-basic.js"></script>
<script src="src/theme.js"></script>
</head>
<body>
</head>
<body>
<div id="about-paint" style="display: none">
<h1 id="about-paint-header">
<img src="images/icons/32x32.png" width="32" height="32" id="paint-32x32" alt=""/>
<img src="images/icons/32x32.png" width="32" height="32" id="paint-32x32" alt="" />
<span id="jspaint-project-name">JS Paint</span>
<!-- <small id="jspaint-version" title="expect bugs!">Alpha</small> -->
<button id="view-project-news">What's New?</button>
@ -84,10 +87,12 @@
There's a new version of JS Paint. <a id="refresh-to-update" href=".">Refresh</a> to get it.
</div>
<div class="on-third-party-host">
This instance of JS Paint is outdated compared to <a href="https://jspaint.app" target="_blank">jspaint.app</a>.
This instance of JS Paint is outdated compared to <a href="https://jspaint.app"
target="_blank">jspaint.app</a>.
</div>
<div class="on-dev-host">
This version of JS Paint is outdated compared to <a href="https://jspaint.app" target="_blank">jspaint.app</a>.
This version of JS Paint is outdated compared to <a href="https://jspaint.app"
target="_blank">jspaint.app</a>.
</div>
</div>
<div id="checking-for-updates" hidden>
@ -102,10 +107,12 @@
</div>
<p>JS Paint is a web-based remake of MS Paint by <a href="https://isaiahodhner.io/">Isaiah Odhner</a>.</p>
<p>Read about the project and extra features on <a href="https://github.com/1j01/jspaint#readme">the readme</a>.</p>
<p>Read about the project and extra features on <a href="https://github.com/1j01/jspaint#readme">the readme</a>.
</p>
<p>Request features and report bugs <a href="https://github.com/1j01/jspaint/issues">on GitHub</a>
or <a href="mailto:isaiahodhner@gmail.com?subject=JS%20Paint">by email</a>.</p>
<p>Support the project at <a href="https://www.paypal.me/IsaiahOdhner" target="_blank">paypal.me/IsaiahOdhner</a>.</p>
<p>Support the project at <a href="https://www.paypal.me/IsaiahOdhner"
target="_blank">paypal.me/IsaiahOdhner</a>.</p>
</div>
<div id="news" hidden>
<!-- <article class="news-entry" id="news-2021-saving">
@ -155,7 +162,8 @@
<article class="news-entry" id="news-2021-saving">
<h1>The GUIcci Update</h1>
<time datetime="2021-12-08">2021-12-08</time>
<img width="640" height="360" style="max-width: 100%; height: auto; image-rendering: auto;" alt="" src="https://i.postimg.cc/tgBncKfJ/guicii-update.png"/>
<img width="640" height="360" style="max-width: 100%; height: auto; image-rendering: auto;" alt=""
src="https://i.postimg.cc/tgBncKfJ/guicii-update.png" />
<h2>New Features</h2>
<p>
<b>View > Zoom > Show Thumbnail</b> to show a preview of the image at a small size, great for pixel art.
@ -166,7 +174,8 @@
</p>
<p>
<b>Alt+Mousewheel</b> to zoom in and out quickly on desktop.
Unlike the Magnifier tool, this allows you to zoom while making (or moving) a selection, for added precision.
Unlike the Magnifier tool, this allows you to zoom while making (or moving) a selection, for added
precision.
</p>
<p>
Added <b>View > Fullscreen</b> to toggle fullscreen mode. This is nice for using JS Paint on your phone.
@ -181,25 +190,34 @@
you can now dock it back when dragging the titlebar.
Previously to dock it you had to double click the titlebar, or drag it by the edge of the window.
</p>
<img alt="Area that you have to click to drag a toolbar out into a window (in green)" src="https://i.postimg.cc/7LB18Gcg/toolbar-drag-out-area.png" width="480" height="380" style="max-width: 100%; height: auto; image-rendering: auto;"/>
<img alt="Area that you have to click to drag a toolbar out into a window (in green)"
src="https://i.postimg.cc/7LB18Gcg/toolbar-drag-out-area.png" width="480" height="380"
style="max-width: 100%; height: auto; image-rendering: auto;" />
<p>
<b>Menus</b> are now fully keyboard (and screen reader) accessible.
In particular, you can hold <kbd>Alt</kbd> and press the access key of a menu button to open the menu,
and then (without <kbd>Alt</kbd>) press the access key of a menu item to select it.
The access key of an item is the underlined letter, or the first letter of the item's text if there's no underline.
The access key of an item is the underlined letter, or the first letter of the item's text if there's no
underline.
</p>
<p>
<b>Error details</b> are now hidden by default in error dialogs.
The details may be more overwhelming than useful in a lot of cases,
but if you need them, you can expand the details.
</p>
<img alt="Example error message box saying 'File not found', with details collapsed." src="https://i.postimg.cc/ZR1qpVGw/file-not-found.png" width="408" height="145" style="max-width: 100%; height: auto; image-rendering: auto;">
<img alt="Example error message box saying 'File not found', with details collapsed."
src="https://i.postimg.cc/ZR1qpVGw/file-not-found.png" width="408" height="145"
style="max-width: 100%; height: auto; image-rendering: auto;">
<p>
<b>File > Exit</b> now exits to the official web desktop,
<a target="_blank" href="https://98.js.org"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42qWTMW7DMAxFKYC+RC/hQR065xSdOyRDr9CxV8gQDzlK5g7RkLmn6CwCCimSchIoWkrAgESTj5+kHeCfFnpORCyPPiLqxgYP9gC/ZyKYEIG+CPAbn0JCr6okizmgvmdIF8BZxUhNgVdv8k3FvCdIu/u228VBnkiUGTZBzgyb1CcASBqdlhAMUEx6aAokWc8KqGCGzB+0lmMTyI0cBUmSV28zMRXzT4bECiI/l+NUZ/J0Cz0TACx6Tiwej9jfQkvIufZ8eVM1tM+1uiTHyP5P7H9IccvtxGCVSg0W6ZDUdz6cYbe8DgAHKKf3P9j8bnhQ0rSJY0DcOm2gwCFSugKqozT51V704xoDQItFWMeTFp+ZbQEGtm4o3/hsoLa1IaC3ocf/4QotsZxI//135gAAAABJRU5ErkJggg==" width="16" height="16" alt=""> 98.js.org</a>,
<a target="_blank" href="https://98.js.org"><img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42qWTMW7DMAxFKYC+RC/hQR065xSdOyRDr9CxV8gQDzlK5g7RkLmn6CwCCimSchIoWkrAgESTj5+kHeCfFnpORCyPPiLqxgYP9gC/ZyKYEIG+CPAbn0JCr6okizmgvmdIF8BZxUhNgVdv8k3FvCdIu/u228VBnkiUGTZBzgyb1CcASBqdlhAMUEx6aAokWc8KqGCGzB+0lmMTyI0cBUmSV28zMRXzT4bECiI/l+NUZ/J0Cz0TACx6Tiwej9jfQkvIufZ8eVM1tM+1uiTHyP5P7H9IccvtxGCVSg0W6ZDUdz6cYbe8DgAHKKf3P9j8bnhQ0rSJY0DcOm2gwCFSugKqozT51V704xoDQItFWMeTFp+ZbQEGtm4o3/hsoLa1IaC3ocf/4QotsZxI//135gAAAABJRU5ErkJggg=="
width="16" height="16" alt=""> 98.js.org</a>,
a re-creation of Windows 98, full of games and applications.
</p>
<img class="inset-deep" src="https://i.postimg.cc/SKHrYpx3/98-js-org-screenshot.png" alt="The 98.js desktop featuring desktop icons, a taskbar, and various application windows." style="max-width: 100%; height: auto; image-rendering: auto;">
<img class="inset-deep" src="https://i.postimg.cc/SKHrYpx3/98-js-org-screenshot.png"
alt="The 98.js desktop featuring desktop icons, a taskbar, and various application windows."
style="max-width: 100%; height: auto; image-rendering: auto;">
<p>
This project spun out of JS Paint, and I have implemented now
Sound Recorder, Notepad, Calculator, and even Windows Explorer,
@ -244,17 +262,20 @@
</p>
<h2>Fixes</h2>
<p>
<b>Menu buttons</b> are easier to open on a touch screen. Sometimes you had to tap twice before the menu opened.
<b>Menu buttons</b> are easier to open on a touch screen. Sometimes you had to tap twice before the menu
opened.
</p>
<p>
Fixed <b>large square brush</b> continuity (it left gaps before, due to a half-implemented optimization).
Fixed <b>large square brush</b> continuity (it left gaps before, due to a half-implemented
optimization).
</p>
<p>
The <b>selection and textboxes</b> no longer "blow up" if you resize them to a minimal size.
They are now limited when you drag an edge past the opposite edge.
</p>
<p>
Fixed a bug where vertically thin selections were difficult or impossible to drag (despite showing a drag cursor).
Fixed a bug where vertically thin selections were difficult or impossible to drag (despite showing a
drag cursor).
(The draggable region was offset outside of the selection box.)
Fixed a similar bug where tool previews would get offset if the canvas's height was very small.
</p>
@ -264,7 +285,8 @@
It's now considerably smarter than Windows 10 about where it lets you drag handles from.
</p>
<p>
In <b>Image > Flip/Rotate</b>, you can now click the custom degrees input field before selecting the "Rotate by angle" option.
In <b>Image > Flip/Rotate</b>, you can now click the custom degrees input field before selecting the
"Rotate by angle" option.
</p>
<p>
The magnifier preview and other tool previews are now hidden while dragging the Colors box or Tools box.
@ -272,8 +294,10 @@
the preview outline for dragging/docking a tool window.
</p>
<p>
For languages that read <b>right-to-left</b>, the History view (<b>Edit > History</b>) now uses a right-to-left layout,
and the color box and tool box no longer flip their layout when dragging them into a window or docking them back to a side of the application.
For languages that read <b>right-to-left</b>, the History view (<b>Edit > History</b>) now uses a
right-to-left layout,
and the color box and tool box no longer flip their layout when dragging them into a window or docking
them back to a side of the application.
</p>
<p>
The history view and error messages use <b>more localized text</b>.
@ -282,7 +306,8 @@
Fixed <b>cut off icons</b> in buttons in the help window toolbar in the Modern theme.
</p>
<p>
All windows now have a default-focused control, and the last focused control in the window is remembered for when you refocus the window.
All windows now have a default-focused control, and the last focused control in the window is remembered
for when you refocus the window.
</p>
<p>
<b>File > New</b> and <b>File > Open</b> now create a new autosave session,
@ -313,20 +338,26 @@
who will then smile a nasty smile and steal Christmas from you.
You can get it back with <b>Extras > Theme > Winter</b>.
</p>
<img class="inset-deep" alt="Winter theme screenshot with candy canes and a text box saying Enjoy!" src="https://i.postimg.cc/SxFtjy8z/winter-theme-enjoy.png" width="440" height="380" style="max-width: 100%; height: auto; image-rendering: auto;"/>
<img class="inset-deep" alt="Winter theme screenshot with candy canes and a text box saying Enjoy!"
src="https://i.postimg.cc/SxFtjy8z/winter-theme-enjoy.png" width="440" height="380"
style="max-width: 100%; height: auto; image-rendering: auto;" />
</article>
<article class="news-entry" id="news-2020-accessibility-update">
<h1>The Accessibility Update</h1>
<time datetime="2020-12-20">2020-12-20</time>
<img width="965" height="399" style="max-width: 100%; height: auto; image-rendering: auto;" alt="Hello in several languages, eye gaze guiding a cursor, and a sea lion barking into a microphone." src="https://i.postimg.cc/j29yrZbm/untitled-23.png"/>
<img width="965" height="399" style="max-width: 100%; height: auto; image-rendering: auto;"
alt="Hello in several languages, eye gaze guiding a cursor, and a sea lion barking into a microphone."
src="https://i.postimg.cc/j29yrZbm/untitled-23.png" />
<h2>Multi-Lingual Support</h2>
<p>JS Paint is now largely localized into 26 languages.</p>
<p>
How am I releasing so many languages at the initial release of multi-lingual support, you may ask?
Well, this project has the somewhat unique opportunity to reuse localizations from an existing program, since it's primarily a remake of MS Paint.
Well, this project has the somewhat unique opportunity to reuse localizations from an existing program,
since it's primarily a remake of MS Paint.
</p>
<p>
I downloaded and installed <a target="_blank" href="https://postimg.cc/4Y1V24wN">26 versions of Windows 98 in virtual machines</a>,
I downloaded and installed <a target="_blank" href="https://postimg.cc/4Y1V24wN">26 versions of Windows
98 in virtual machines</a>,
and extracted text from mspaint.exe in each one of them,
using a set of scripts that I wrote to to help me automate the process.
</p>
@ -334,24 +365,34 @@
To change the language, go to <b>Extras > Language</b>.
Your preferred language may already be detected, if specified in system or browser settings.
</p>
<img width="1280" height="720" style="max-width: 100%; height: auto; image-rendering: auto;" alt="26 languages, right off the bat!" src="https://i.postimg.cc/G2bH92fp/language-menu.png"/>
<img width="1280" height="720" style="max-width: 100%; height: auto; image-rendering: auto;"
alt="26 languages, right off the bat!" src="https://i.postimg.cc/G2bH92fp/language-menu.png" />
<p>For Arabic and Hebrew, right-to-left layout is supported!</p>
<p>I tried my hand at some Arabic calligraphy...</p>
<img width="1141" height="800" style="max-width: 100%; height: auto; image-rendering: auto;" alt="Calligraphy where the shapes of the tools in Paint make up Arabic words for them." src="https://i.postimg.cc/NFX2TTp1/untitled-45.png"/>
<img width="1141" height="800" style="max-width: 100%; height: auto; image-rendering: auto;"
alt="Calligraphy where the shapes of the tools in Paint make up Arabic words for them."
src="https://i.postimg.cc/NFX2TTp1/untitled-45.png" />
<p>
If you want to contribute translations, <a target="_blank" href="https://github.com/1j01/jspaint/issues/80">get in touch!</a>
If you want to contribute translations, <a target="_blank"
href="https://github.com/1j01/jspaint/issues/80">get in touch!</a>
I need to do some technical work to set up for community translations on a public platform,
but I'm glad people have already expressed interest in helping translate!
(I also want to simplify the language in various parts of the UI before asking people to translate them.)
(I also want to simplify the language in various parts of the UI before asking people to translate
them.)
</p>
<h2>Eye Gaze Mode</h2>
<img width="511" height="360" style="max-width: 100%; height: auto; image-rendering: auto;" alt="You can use eye gaze or head movements to control the cursor." src="https://i.postimg.cc/2yC137gc/20.png"/>
<img width="511" height="360" style="max-width: 100%; height: auto; image-rendering: auto;"
alt="You can use eye gaze or head movements to control the cursor."
src="https://i.postimg.cc/2yC137gc/20.png" />
<p>Eye Gaze Mode lets you control JS Paint without using your hands.</p>
<p>It's intended for use with an eye tracker, head tracker, or other coarse input scenario.</p>
<p>You don't need a thousand-dollar eye tracker device to play around with this, just a webcam and some free software.</p>
<p>You don't need a thousand-dollar eye tracker device to play around with this, just a webcam and some free
software.</p>
<p>
I recommend <a target="_blank" href="https://eviacam.crea-si.com/">Enable Viacam</a>, which is <em>not</em> an eye gaze tracker,
but rather a general video movement tracker that you can set up to track your head movement (or your torso or hand or anything else).
I recommend <a target="_blank" href="https://eviacam.crea-si.com/">Enable Viacam</a>, which is
<em>not</em> an eye gaze tracker,
but rather a general video movement tracker that you can set up to track your head movement (or your
torso or hand or anything else).
</p>
<p>
Eye tracking via a webcam has a ways to go, but it's also pretty amazing in its own right.
@ -370,19 +411,24 @@
It can also be frustrating, and takes some practice to master.
</p>
<p>
A good place to start is coloring line art using just the Fill tool (<img src="help/p_paint.gif" width="15" height="11" alt="">):
A good place to start is coloring line art using just the Fill tool (<img src="help/p_paint.gif"
width="15" height="11" alt="">):
</p>
<ul>
<li>
<a target="_blank" href="https://duckduckgo.com/?t=canonical&q=coloring+pages&atb=v232-1&iax=images&ia=images">Find coloring pages online</a>,
<a target="_blank"
href="https://duckduckgo.com/?t=canonical&q=coloring+pages&atb=v232-1&iax=images&ia=images">Find
coloring pages online</a>,
and copy and paste them into JS Paint.
</li>
<li>
You can convert them to black and white in <b>Image > Attributes</b>, and then switch back to Colors.
You can convert them to black and white in <b>Image > Attributes</b>, and then switch back to
Colors.
(This makes it work better with the Fill tool.)
</li>
<li>
Enable Eye Gaze Mode with <b>Extras > Eye Gaze Mode</b> and note that it will start clicking where you hover.
Enable Eye Gaze Mode with <b>Extras > Eye Gaze Mode</b> and note that it will start clicking where
you hover.
You can disable this dwell clicking with the eye icon in the bottom of the screen.
</li>
<li>
@ -394,9 +440,12 @@
I decided to make this available as a separate option. Access with <b>Extras > Vertical Color Box</b>.
</p>
<h2>Speech Recognition</h2>
<img width="549" height="275" style="max-width: 100%; height: auto; image-rendering: auto;" alt="The sea lion says &quot;Art! Art! Art!&quot; into a microphone." src="https://i.postimg.cc/BnQ8cpY2/Art-Art-Art.png"/>
<img width="549" height="275" style="max-width: 100%; height: auto; image-rendering: auto;"
alt="The sea lion says &quot;Art! Art! Art!&quot; into a microphone."
src="https://i.postimg.cc/BnQ8cpY2/Art-Art-Art.png" />
<p>
Using only your voice, you can switch tools and colors, pan the view, click on buttons on the screen by name, and use most menu items.
Using only your voice, you can switch tools and colors, pan the view, click on buttons on the screen by
name, and use most menu items.
You can even say "draw a cat in a party hat" to have JS Paint try to sketch a cat in a party hat.
</p>
<p>This feature pairs well with Eye Gaze Mode for a more complete hands free experience.</p>
@ -404,33 +453,43 @@
The feature is only available on Chrome, and only understands English.
Note that Chrome sends your voice to Google servers.
</p>
<p>Access with <b>Extras > Speech Recognition</b>. If this option is grayed out, your browser is not supported.</p>
<p>Access with <b>Extras > Speech Recognition</b>. If this option is grayed out, your browser is not
supported.</p>
<p>JS Paint will show what it thinks you said in the status bar at the bottom of the screen.</p>
<p>
There are many synonyms for commands, and often you can do things with very short phrases like "Curve" to switch to the Curve tool.
If it's not recognizing your voice for short commands like "Curve" or "Cut", you may want to try longer phrases like "Curve tool" or "Cut selection",
There are many synonyms for commands, and often you can do things with very short phrases like "Curve"
to switch to the Curve tool.
If it's not recognizing your voice for short commands like "Curve" or "Cut", you may want to try longer
phrases like "Curve tool" or "Cut selection",
as this helps it distinguish the sound as speech, rather than a cough for instance.
</p>
<h2>Edit Colors Dialog</h2>
<p>I also implemented the Edit Colors dialog. Previously this used the native system color picker, and didn't work for some people.</p>
<p>I also implemented the Edit Colors dialog. Previously this used the native system color picker, and
didn't work for some people.</p>
<p>Access with <b>Colors > Edit Colors</b> or double click a color in the palette to edit.</p>
<figure>
<img width="496" height="350" style="max-width: 100%; height: auto; image-rendering: auto;" src="https://i.postimg.cc/cLNgWH0r/Peek-2020-12-04-00-31.gif"/>
<figcaption>An animation morphing between JS Paint and MS Paint's color picking dialog. It's pretty close, other than the font.</figcaption>
<img width="496" height="350" style="max-width: 100%; height: auto; image-rendering: auto;"
src="https://i.postimg.cc/cLNgWH0r/Peek-2020-12-04-00-31.gif" />
<figcaption>An animation morphing between JS Paint and MS Paint's color picking dialog. It's pretty
close, other than the font.</figcaption>
</figure>
<p>Keyboard shortcuts are supported in this dialog, and for mobile devices with small screens, I made it treat adding custom colors as a separate screen.</p>
<p>Keyboard shortcuts are supported in this dialog, and for mobile devices with small screens, I made it
treat adding custom colors as a separate screen.</p>
<h2>Conclusion</h2>
<p>
JS Paint should be way more accessible now. And futuristic.
<p>
<p>
Of course there's always more that could be done.
Eye Gaze Mode could use brush stroke smoothing, and Speech Recognition could use Artificial General Intelligence.
Eye Gaze Mode could use brush stroke smoothing, and Speech Recognition could use Artificial General
Intelligence.
</p>
<p>
I'd love to see people using JS Paint, especially the Eye Gaze Mode and Speech Recognition,
so if you record a video of using JS Paint, please
<a target="_blank" href="https://docs.google.com/forms/d/e/1FAIpQLSdGgS6TS4wBV89v8NoYHenh1eI8jYBfgwYBdPx-OaCEG5EW7g/viewform?usp=sf_link">send it to me through this form.</a>
<a target="_blank"
href="https://docs.google.com/forms/d/e/1FAIpQLSdGgS6TS4wBV89v8NoYHenh1eI8jYBfgwYBdPx-OaCEG5EW7g/viewform?usp=sf_link">send
it to me through this form.</a>
This lets me know what's actually important to people, and what's confusing,
and it gives me motivation to work on new features.
</p>
@ -438,17 +497,20 @@
<article class="news-entry" id="news-2019-winter-update">
<h1>Winter Update</h1>
<time datetime="2019-12-20">2019-12-20</time>
<img width="563" height="334" style="max-width: 100%; height: auto; image-rendering: auto;" alt="" src="https://i.postimg.cc/63Wc6vpG/2019-winter-update-candy-cane.gif"/>
<img width="563" height="334" style="max-width: 100%; height: auto; image-rendering: auto;" alt=""
src="https://i.postimg.cc/63Wc6vpG/2019-winter-update-candy-cane.gif" />
<h2>Winter Theme</h2>
<p>
A new UI skin is available, under <b>Extras &gt; Themes &gt; Winter</b>, featuring winter and holiday icons, festive fonts, and a palette with seasonal colors and peppermint patterns.
A new UI skin is available, under <b>Extras &gt; Themes &gt; Winter</b>, featuring winter and holiday
icons, festive fonts, and a palette with seasonal colors and peppermint patterns.
</p>
<img width="256" height="16" alt="" src="images/winter/tools.png"/>
<img width="256" height="16" alt="" src="images/winter/tools.png" />
<p>
Merry Christmas and happy Hanukkah!
</p>
<h2>Better History</h2>
<b class="new">New:</b> Jump to any point in the document's history, forwards or backwards, with <b>Edit &gt; History</b> or <kbd>Ctrl+Shift+Y</kbd>.
<b class="new">New:</b> Jump to any point in the document's history, forwards or backwards, with <b>Edit
&gt; History</b> or <kbd>Ctrl+Shift+Y</kbd>.
<ul>
<li>
Click on Text in the history view to go back to text editing.
@ -457,14 +519,17 @@
You can return to when a selection existed.
</li>
<li>
Note: these states are skipped over with normal Undo and Redo, so you need to use the History window.
Note: these states are skipped over with normal Undo and Redo, so you need to use the History
window.
</li>
<li>
Branching history: if you undo, and then make changes, you can get back to everything.
Future states are preserved.
</li>
</ul>
<b class="bad">Warning:</b> History is not saved with the autosave. Document history will be lost if you refresh the page, or close the tab, or if the tab crashes, or if you close or restart your browser, or likely if you're just on a phone and the mobile browser loses focus.
<b class="bad">Warning:</b> History is not saved with the autosave. Document history will be lost if you
refresh the page, or close the tab, or if the tab crashes, or if you close or restart your browser, or
likely if you're just on a phone and the mobile browser loses focus.
<h2>Improved Mobile Support</h2>
<p>
<b class="new">New:</b> Use two fingers to pan the view.
@ -474,7 +539,8 @@
With that, combined with multitouch panning,
JS Paint is much more useable on a phone.
</p>
<p><b class="bad">Caveat:</b> It's slow on some devices, and parts of the interface are still too small for touch.</p>
<p><b class="bad">Caveat:</b> It's slow on some devices, and parts of the interface are still too small for
touch.</p>
</article>
<article class="news-entry" id="news-2019-polygon-text-and-select">
<h1>Polygon, Text, and Select</h1>
@ -485,7 +551,8 @@
This applies to selections, textboxes, and the main canvas handles.
</p>
<p>
Resizing things while zoomed in is <a target="_blank" href="https://github.com/1j01/jspaint/issues/13#issuecomment-562247085">finally fixed</a>!
Resizing things while zoomed in is <a target="_blank"
href="https://github.com/1j01/jspaint/issues/13#issuecomment-562247085">finally fixed</a>!
</p>
<p>
The Text tool now perfectly previews the pixels that will be placed on the canvas.
@ -496,7 +563,8 @@
and <a target="_blank" href="https://jsfiddle.net/1j01/qkvfjn1r/">here</a> if you're interested.)
</p>
<p>
With the fill-only option selected, the Polygon tool now previews with inverted lines, like MS Paint does.
With the fill-only option selected, the Polygon tool now previews with inverted lines, like MS Paint
does.
(When you finish the polygon, the boundary of the shape matches the preview exactly,
because it actually <em>does</em> draw a stroke, just the same color as the fill.)
</p>
@ -522,7 +590,8 @@
<h1>The Grid, Custom Zoom, and Dynamic Cursors</h1>
<time datetime="2019-10-09">2019-10-09</time>
<p>
<b class="new">New:</b> The Grid. Zoom to 4x+ and use <b>View &gt; Zoom &gt; Show Grid</b> or <kbd>Ctrl+G</kbd> to enable.
<b class="new">New:</b> The Grid. Zoom to 4x+ and use <b>View &gt; Zoom &gt; Show Grid</b> or
<kbd>Ctrl+G</kbd> to enable.
This works with browser zoom as well to provide crisp gridlines even if you zoom in with your browser.
</p>
<p>
@ -542,7 +611,8 @@
<h1>Full Clipboard Support</h1>
<time datetime="2019-09-21">2019-09-21</time>
<p>
JS Paint now lets you copy real image data to the Clipboard, both with keyboard shortcuts and from the Edit menu.
JS Paint now lets you copy real image data to the Clipboard, both with keyboard shortcuts and from the
Edit menu.
This feature is available in Chrome 76+. Other browsers don't support it yet, as of Sep 2019.
</p>
<p>
@ -555,29 +625,35 @@
background: white;
color: black;
}
.news-entry {
padding: 20px;
max-width: 563px;
}
.news-entry > h1 {
.news-entry>h1 {
font-size: 1.3em;
margin: 0;
margin-bottom: 0.3em;
}
.news-entry > time {
.news-entry>time {
font-size: 1.2em;
color: gray;
}
.news-entry > h2 {
.news-entry>h2 {
font-size: 1.9em;
font-weight: normal;
margin: 0;
margin-bottom: 0.3em;
margin-top: 0.3em;
}
.news-entry .new {
color: green;
}
.news-entry .bad {
color: #d11a29;
}
@ -602,7 +678,8 @@
<script src="lib/os-gui/MenuBar.js"></script>
<script src="lib/imagetracer_v1.2.5.js"></script>
<script src="src/app-localization.js"></script> <!-- must not be async/deferred, as it uses document.write(); and must come before other app code which uses localization functions -->
<script src="src/app-localization.js"></script>
<!-- must not be async/deferred, as it uses document.write(); and must come before other app code which uses localization functions -->
<script src="src/msgbox.js"></script>
<script src="src/functions.js"></script>
<script src="src/helpers.js"></script>
@ -640,7 +717,7 @@
<script src="src/vaporwave-fun.js"></script>
<noscript>
<h1><img src="images/icons/32x32.png" width="32" height="32" alt=""/> JS Paint</h1>
<h1><img src="images/icons/32x32.png" width="32" height="32" alt="" /> JS Paint</h1>
<p>This application requires JavaScript to run.</p>
@ -656,50 +733,42 @@
<svg style="position: absolute; pointer-events: none; bottom: 100%;">
<defs>
<filter id="disabled-inset-filter" x="0" y="0" width="1px" height="1px">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="
<feColorMatrix in="SourceGraphic" type="matrix" values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
-1000 -1000 -1000 1 0
"
result="black-parts-isolated"
/>
<feFlood result="shadow-color" flood-color="var(--ButtonShadow)"/>
<feFlood result="hilight-color" flood-color="var(--ButtonHilight)"/>
<feOffset in="black-parts-isolated" dx="1" dy="1" result="offset"/>
<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset"/>
<feComposite in="shadow-color" in2="black-parts-isolated" operator="in" result="shadow-colored"/>
" result="black-parts-isolated" />
<feFlood result="shadow-color" flood-color="var(--ButtonShadow)" />
<feFlood result="hilight-color" flood-color="var(--ButtonHilight)" />
<feOffset in="black-parts-isolated" dx="1" dy="1" result="offset" />
<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset" />
<feComposite in="shadow-color" in2="black-parts-isolated" operator="in" result="shadow-colored" />
<feMerge>
<feMergeNode in="hilight-colored-offset"/>
<feMergeNode in="shadow-colored"/>
<feMergeNode in="hilight-colored-offset" />
<feMergeNode in="shadow-colored" />
</feMerge>
</filter>
<filter id="disabled-inset-filter-2" x="0" y="0" width="1px" height="1px">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="
<feColorMatrix in="SourceGraphic" type="matrix" values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
-1 -1 -0 1 0
"
result="black-and-blue-parts-isolated"
/>
<feFlood result="shadow-color" flood-color="var(--ButtonShadow)"/>
<feFlood result="hilight-color" flood-color="var(--ButtonHilight)"/>
<feOffset in="black-and-blue-parts-isolated" dx="1" dy="1" result="offset"/>
<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset"/>
<feComposite in="shadow-color" in2="black-and-blue-parts-isolated" operator="in" result="shadow-colored"/>
" result="black-and-blue-parts-isolated" />
<feFlood result="shadow-color" flood-color="var(--ButtonShadow)" />
<feFlood result="hilight-color" flood-color="var(--ButtonHilight)" />
<feOffset in="black-and-blue-parts-isolated" dx="1" dy="1" result="offset" />
<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset" />
<feComposite in="shadow-color" in2="black-and-blue-parts-isolated" operator="in"
result="shadow-colored" />
<feMerge>
<feMergeNode in="hilight-colored-offset"/>
<feMergeNode in="shadow-colored"/>
<feMergeNode in="hilight-colored-offset" />
<feMergeNode in="shadow-colored" />
</feMerge>
</filter>
</defs>
</svg>
</body>
</body>
</html>

View File

@ -1,13 +1,13 @@
/** 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_canvas = make_canvas();
$(swatch_canvas).css({pointerEvents: "none"}).appendTo($swatch);
$(swatch_canvas).css({ pointerEvents: "none" }).appendTo($swatch);
// @TODO: clean up event listener
$G.on("theme-load", ()=> { update_$swatch($swatch); });
$G.on("theme-load", () => { update_$swatch($swatch); });
$swatch.data("swatch", color);
update_$swatch($swatch, color);
@ -37,7 +37,7 @@ function update_$swatch($swatch, new_color) {
});
}
function $ColorBox(vertical){
function $ColorBox(vertical) {
const $cb = $(E("div")).addClass("color-box");
const $current_colors = $Swatch(selected_colors.ternary).addClass("current-colors");
@ -77,7 +77,7 @@ function $ColorBox(vertical){
// @TODO: allow metaKey for ternary color, and selection cropping, on macOS?
ctrl = e.ctrlKey;
button = e.button;
if(button === 0){
if (button === 0) {
$c.data("$last_fg_color_button", $b);
}
@ -113,13 +113,13 @@ function $ColorBox(vertical){
$some_button.outerHeight() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-top")) +
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 {
const width_per_button =
$some_button.outerWidth() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-left")) +
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
@ -129,7 +129,7 @@ function $ColorBox(vertical){
if (vertical) {
$c = $Component(localize("Colors"), "colors-component", "tall", $cb);
$c.appendTo(get_direction() === "rtl" ? $left : $right); // opposite ToolBox by default
}else{
} else {
$c = $Component(localize("Colors"), "colors-component", "wide", $cb);
$c.appendTo($bottom);
}

View File

@ -5,8 +5,8 @@
function get_segments(component_area_el, pos_axis, exclude_component_el) {
const $other_components = $(component_area_el).find(".component").not(exclude_component_el);
return $other_components.toArray().map((component_el)=> {
const segment = {element: component_el};
return $other_components.toArray().map((component_el) => {
const segment = { element: component_el };
if (pos_axis === "top") {
segment.pos = component_el.offsetTop;
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) {
segments.sort((a, b)=> a.pos - b.pos);
segments.sort((a, b) => a.pos - b.pos);
// Clamp
for (const segment of segments) {
@ -72,7 +72,7 @@ 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
const $c = $(E("div")).addClass("component");
$c.addClass(className);
@ -89,15 +89,15 @@ function $Component(title, className, orientation, $el){
}[orientation]);
// 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(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px");
}
let iid;
if($("body").hasClass("eye-gaze-mode")){
if ($("body").hasClass("eye-gaze-mode")) {
// @TODO: don't use an interval for this!
iid = setInterval(()=> {
iid = setInterval(() => {
const scale = 3;
$c.css({
transform: `scale(${scale})`,
@ -118,11 +118,11 @@ function $Component(title, className, orientation, $el){
let $dock_to;
let $ghost;
if(orientation === "tall"){
if (orientation === "tall") {
pos_axis = "top";
}else if(get_direction() === "rtl"){
} else if (get_direction() === "rtl") {
pos_axis = "right";
}else{
} else {
pos_axis = "left";
}
@ -176,14 +176,14 @@ function $Component(title, className, orientation, $el){
apply_segments(component_area_el, pos_axis, segments);
};
$w.on("window-drag-start", (e)=> {
$w.on("window-drag-start", (e) => {
e.preventDefault();
});
const imagine_window_dimensions = ()=> {
const imagine_window_dimensions = () => {
const prev_window_shown = $w.is(":visible");
$w.show();
let $spacer;
let {offsetLeft, offsetTop} = $c[0];
let { offsetLeft, offsetTop } = $c[0];
if ($c.closest(".tool-window").length == 0) {
const styles = getComputedStyle($c[0]);
$spacer = $(E("div")).addClass("component").css({
@ -194,7 +194,7 @@ function $Component(title, className, orientation, $el){
// let padding be influenced by CSS
});
$w.append($spacer);
({offsetLeft, offsetTop} = $spacer[0]);
({ offsetLeft, offsetTop } = $spacer[0]);
}
const rect = $w[0].getBoundingClientRect();
if ($spacer) {
@ -206,11 +206,11 @@ function $Component(title, className, orientation, $el){
const w_styles = getComputedStyle($w[0]);
offsetLeft += parseFloat(w_styles.borderLeftWidth);
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) {
return {rect: $c[0].getBoundingClientRect()};
return { rect: $c[0].getBoundingClientRect() };
}
const styles = getComputedStyle($c[0]);
const $spacer = $(E("div")).addClass("component").css({
@ -223,18 +223,18 @@ function $Component(title, className, orientation, $el){
if ($spacer) {
$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
// so the alternating pattern of the border is unbroken
w = (~~(rect.width/2))*2 + 1;
h = (~~(rect.height/2))*2 + 1;
w = (~~(rect.width / 2)) * 2 + 1;
h = (~~(rect.height / 2)) * 2 + 1;
if(!$ghost){
if (!$ghost) {
$ghost = $(E("div")).addClass("component-ghost dock");
$ghost.appendTo("body");
}
@ -248,24 +248,24 @@ function $Component(title, className, orientation, $el){
top: e.clientY + ($dock_to ? oy : oy2) + inset,
});
if($dock_to){
if ($dock_to) {
$ghost.addClass("dock");
}else{
} else {
$ghost.removeClass("dock");
}
};
$c.add($w.$titlebar).on("pointerdown", e => {
// 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 =
$c.is(e.target) ||
(
$(e.target).closest($w.$titlebar).length > 0 &&
$(e.target).closest("button").length === 0
);
if(!validTarget){ return; }
if (!validTarget) { return; }
// 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 rect = $c[0].getBoundingClientRect();
@ -274,19 +274,19 @@ function $Component(title, className, orientation, $el){
ox = -Math.min(Math.max(-ox, 0), docked.rect.width);
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;
oy2 = rect.top - offsetTop - e.clientY;
$("body").addClass("dragging");
$("body").css({cursor: "default"}).addClass("cursor-bully");
$("body").css({ cursor: "default" }).addClass("cursor-bully");
$G.on("pointermove", drag_update_position);
$G.one("pointerup", e => {
$G.off("pointermove", drag_update_position);
drag_onpointerup(e);
$("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
});
@ -305,31 +305,31 @@ function $Component(title, className, orientation, $el){
$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_top = e.clientY + oy;
const dock_ghost_right = dock_ghost_left + width;
const dock_ghost_bottom = dock_ghost_top + height;
const q = 5;
if(orientation === "tall"){
if (orientation === "tall") {
pos_axis = "top";
if(dock_ghost_left-q < $left[0].getBoundingClientRect().right){
if (dock_ghost_left - q < $left[0].getBoundingClientRect().right) {
$dock_to = $left;
}
if(dock_ghost_right+q > $right[0].getBoundingClientRect().left){
if (dock_ghost_right + q > $right[0].getBoundingClientRect().left) {
$dock_to = $right;
}
}else{
} else {
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;
}
if(dock_ghost_bottom+q > $bottom[0].getBoundingClientRect().top){
if (dock_ghost_bottom + q > $bottom[0].getBoundingClientRect().top) {
$dock_to = $bottom;
}
}
if($dock_to){
if ($dock_to) {
const dock_to_rect = $dock_to[0].getBoundingClientRect();
pos = (
pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left
@ -349,15 +349,15 @@ function $Component(title, className, orientation, $el){
$w.hide();
// 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
$last_docked_to = $c.parent();
if($dock_to){
if ($dock_to) {
last_docked_to_pos = pos;
}
}
if($dock_to){
if ($dock_to) {
// Dock component to $dock_to
dock_to($dock_to);
} else {
@ -378,7 +378,7 @@ function $Component(title, className, orientation, $el){
$c.show = () => {
$($c[0]).show(); // avoid recursion
if($.contains($w[0], $c[0])){
if ($.contains($w[0], $c[0])) {
$w.show();
}
return $c;
@ -388,14 +388,14 @@ function $Component(title, className, orientation, $el){
return $c;
};
$c.toggle = () => {
if($c.is(":visible")){
if ($c.is(":visible")) {
$c.hide();
}else{
} else {
$c.show();
}
return $c;
};
$c.destroy = ()=> {
$c.destroy = () => {
$w.close();
$c.remove();
clearInterval(iid);

View File

@ -1,5 +1,5 @@
function $FontBox(){
function $FontBox() {
const $fb = $(E("div")).addClass("font-box");
const $family = $(E("select")).addClass("inset-deep").attr({
@ -85,7 +85,7 @@ function $FontBox(){
$button.attr("aria-pressed", $button.hasClass("selected"));
update_font();
});
if(text_tool_font[thing]){
if (text_tool_font[thing]) {
$button.addClass("selected").attr("aria-pressed", true);
}
return $button;

View File

@ -1,6 +1,6 @@
let theme_dev_blob_url;
function $ToolBox(tools, is_extras){
function $ToolBox(tools, is_extras) {
const $tools = $(E("div")).addClass("tools");
const $tool_options = $(E("div")).addClass("tool-options");
@ -19,7 +19,7 @@ function $ToolBox(tools, is_extras){
const $icon = $(E("span")).addClass("tool-icon");
$icon.appendTo($b);
const update_css = ()=> {
const update_css = () => {
const use_svg = !theme_dev_blob_url && (
(
(window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0)
@ -46,9 +46,9 @@ function $ToolBox(tools, is_extras){
select_tool(tool, true);
return;
}
if(selected_tool === tool && tool.deselect){
if (selected_tool === tool && tool.deselect) {
select_tools(return_to_tools);
}else{
} else {
select_tool(tool);
}
});
@ -58,9 +58,9 @@ function $ToolBox(tools, is_extras){
showing_tooltips = true;
$status_text.text(tool.description);
};
if(showing_tooltips){
if (showing_tooltips) {
show_tooltip();
}else{
} else {
const tid = setTimeout(show_tooltip, 300);
$b.on("pointerleave", () => {
clearTimeout(tid);
@ -80,7 +80,7 @@ function $ToolBox(tools, is_extras){
$c.appendTo(get_direction() === "rtl" ? $right : $left); // opposite ColorBox by default
$c.update_selected_tool = () => {
$buttons.removeClass("selected");
selected_tools.forEach((selected_tool)=> {
selected_tools.forEach((selected_tool) => {
selected_tool.$button.addClass("selected");
});
$tool_options.children().detach();
@ -102,14 +102,14 @@ function $ToolBox(tools, is_extras){
let dev_theme_tool_icons = false;
try {
dev_theme_tool_icons = localStorage.dev_theme_tool_icons === "true";
// eslint-disable-next-line no-empty
// eslint-disable-next-line no-empty
} catch (e) { }
if (dev_theme_tool_icons) {
let last_update_id = 0;
$G.on("session-update", ()=> {
$G.on("session-update", () => {
last_update_id += 1;
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
if (this_update_id !== last_update_id) {
return;

View File

@ -2,7 +2,7 @@
function make_window_supporting_scale(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")) {
return;
}
@ -36,13 +36,13 @@ function make_window_supporting_scale(options) {
// requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
if(!options.$component){
if (!options.$component) {
$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);
};
const on_close = ()=> {
const on_close = () => {
$w.off("close", on_close);
$G.off("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
};

View File

@ -44,31 +44,31 @@ function Handles(options) {
let dragged = false;
const resizes_height = y_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");
$grab_region.remove();
}else{
} else {
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";
}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";
}else if(resizes_width){
} else if (resizes_width) {
cursor_fname = "ew-resize";
}else if(resizes_height){
} else if (resizes_height) {
cursor_fname = "ns-resize";
}
let fallback_cursor = "";
if(y_axis === HANDLE_TOP){ fallback_cursor += "n"; }
if(y_axis === HANDLE_BOTTOM){ fallback_cursor += "s"; }
if(x_axis === HANDLE_LEFT){ fallback_cursor += "w"; }
if(x_axis === HANDLE_RIGHT){ fallback_cursor += "e"; }
if (y_axis === HANDLE_TOP) { fallback_cursor += "n"; }
if (y_axis === HANDLE_BOTTOM) { fallback_cursor += "s"; }
if (x_axis === HANDLE_LEFT) { fallback_cursor += "w"; }
if (x_axis === HANDLE_RIGHT) { fallback_cursor += "e"; }
fallback_cursor += "-resize";
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) => {
$resize_ghost.appendTo($object_container);
@ -80,22 +80,22 @@ function Handles(options) {
let delta_y = 0;
let width, height;
// @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;
width = ~~(m.x - rect.x);
}else if(x_axis === HANDLE_LEFT){
} else if (x_axis === HANDLE_LEFT) {
delta_x = ~~(m.x - rect.x);
width = ~~(rect.x + rect.width - m.x);
}else{
} else {
width = ~~(rect.width);
}
if(y_axis === HANDLE_BOTTOM){
if (y_axis === HANDLE_BOTTOM) {
delta_y = 0;
height = ~~(m.y - rect.y);
}else if(y_axis === HANDLE_TOP){
} else if (y_axis === HANDLE_TOP) {
delta_y = ~~(m.y - rect.y);
height = ~~(rect.y + rect.height - m.y);
}else{
} else {
height = ~~(rect.height);
}
let new_rect = {
@ -126,16 +126,16 @@ function Handles(options) {
};
$h.add($grab_region).on("pointerdown", event => {
dragged = false;
if(event.button === 0){
if (event.button === 0) {
$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);
$("body").css({cursor: ""}).removeClass("cursor-bully");
$("body").css({ cursor: "" }).removeClass("cursor-bully");
$resize_ghost.remove();
if(dragged){
if (dragged) {
options.set_rect(rect);
}
$handles_container.trigger("update");
@ -154,14 +154,14 @@ function Handles(options) {
const x = get_handles_offset_left();
const y = get_handles_offset_top();
const grab_size = 32;
for ({len_key, pos_key, region, offset} of [
{len_key: "width", pos_key: "left", region: x_axis, offset: x},
{len_key: "height", pos_key: "top", region: y_axis, offset: y},
for ({ len_key, pos_key, region, offset } of [
{ len_key: "width", pos_key: "left", region: x_axis, offset: x },
{ len_key: "height", pos_key: "top", region: y_axis, offset: y },
]) {
let middle_start = Math.max(
rect[len_key] * magnification / 2 - grab_size/2,
rect[len_key] * magnification / 2 - grab_size / 2,
Math.min(
grab_size/2,
grab_size / 2,
rect[len_key] * magnification / 3
)
);
@ -171,9 +171,9 @@ function Handles(options) {
middle_start = 0;
middle_end = magnification;
}
const start_start = -grab_size/2;
const start_start = -grab_size / 2;
const start_end = Math.min(
grab_size/2,
grab_size / 2,
middle_start
);
const end_start = rect[len_key] * magnification - start_end;
@ -197,7 +197,7 @@ function Handles(options) {
[len_key]: middle_end - middle_start,
});
} 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({
[pos_key]: offset + end_start,
[len_key]: end_end - end_start,
@ -216,6 +216,6 @@ function Handles(options) {
this.handles = handles;
// 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.show = ()=> { $(handles).css({opacity: "", pointerEvents: ""}); };
this.hide = () => { $(handles).css({ opacity: 0, pointerEvents: "none" }); };
this.show = () => { $(handles).css({ opacity: "", pointerEvents: "" }); };
}

View File

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

View File

@ -32,7 +32,7 @@ class OnCanvasSelection extends OnCanvasObject {
});
this.position();
const instantiate = ()=> {
const instantiate = () => {
if (img_or_canvas) {
// (this applies when pasting a selection)
// 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,
$object_container: $canvas_area,
outset: 2,
get_rect: ()=> ({x: this.x, y: this.y, width: this.width, height: this.height}),
set_rect: ({x, y, width, height}) => {
get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }),
set_rect: ({ x, y, width, height }) => {
undoable({
name: "Resize Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true,
}, ()=> {
}, () => {
this.x = x;
this.y = y;
this.width = width;
@ -75,20 +75,20 @@ class OnCanvasSelection extends OnCanvasObject {
this.resize();
});
},
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_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
});
let mox, moy;
const pointermove = e => {
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(/^Move Selection$/)),
name: e.shiftKey ? "Smear Selection" : "Move Selection",
update_name: true,
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true,
}, ()=> {
}, () => {
const m = to_canvas_coords(e);
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);
@ -120,7 +120,7 @@ class OnCanvasSelection extends OnCanvasObject {
name: "Stamp Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true,
}, ()=> {
}, () => {
this.draw();
});
}
@ -131,7 +131,7 @@ class OnCanvasSelection extends OnCanvasObject {
name: "Stamp Selection",
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true,
}, ()=> {
}, () => {
this.draw();
});
}
@ -216,10 +216,10 @@ class OnCanvasSelection extends OnCanvasObject {
// @FIXME: work with transparent selected background color
// (support treating partially transparent background colors as transparency)
if (
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+2] - background_color_rgba[2]) <= match_threshold &&
Math.abs(sourceImageData.data[i+3] - background_color_rgba[3]) <= 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 + 2] - background_color_rgba[2]) <= match_threshold &&
Math.abs(sourceImageData.data[i + 3] - background_color_rgba[3]) <= match_threshold
) {
in_cutout = false;
}

View File

@ -37,7 +37,7 @@ class OnCanvasTextBox extends OnCanvasObject {
this.canvas.style.pointerEvents = "none";
this.$el.append(this.canvas);
const update_size = ()=> {
const update_size = () => {
this.position();
this.$el.triggerHandler("update"); // update handles
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.
edit_textarea.style.height = "";
edit_textarea.style.minHeight = "0px";
@ -60,13 +60,13 @@ class OnCanvasTextBox extends OnCanvasObject {
update_size();
};
const update = ()=> {
requestAnimationFrame(()=> {
const update = () => {
requestAnimationFrame(() => {
edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync
});
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.background = tool_transparent_mode ? "transparent" : get_solid_color(selected_colors.background);
this.$editor.add(this.canvas).css({
@ -106,13 +106,13 @@ class OnCanvasTextBox extends OnCanvasObject {
var data_url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg_source)}`;
var img = new Image();
img.onload = ()=> {
img.onload = () => {
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.ctx.drawImage(img, 0, 0);
update_helper_layer(); // @TODO: under-grid specific helper layer?
};
img.onerror = (event)=> {
img.onerror = (event) => {
window.console && console.log("Failed to load image", event);
};
img.src = data_url;
@ -120,8 +120,8 @@ class OnCanvasTextBox extends OnCanvasObject {
$G.on("option-changed", this._on_option_changed = update);
this.$editor.on("input", this._on_input = update);
this.$editor.on("scroll", this._on_scroll = ()=> {
requestAnimationFrame(()=> {
this.$editor.on("scroll", this._on_scroll = () => {
requestAnimationFrame(() => {
edit_textarea.scrollTop = 0; // prevent scrolling edit textarea to keep in sync
});
});
@ -139,8 +139,8 @@ class OnCanvasTextBox extends OnCanvasObject {
$object_container: $canvas_area,
outset: 2,
thick: true,
get_rect: ()=> ({x: this.x, y: this.y, width: this.width, height: this.height}),
set_rect: ({x, y, width, height}) => {
get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }),
set_rect: ({ x, y, width, height }) => {
this.x = x;
this.y = y;
this.width = width;
@ -152,7 +152,7 @@ class OnCanvasTextBox extends OnCanvasObject {
this.canvas.width = width;
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
const old_x = this.x;
const old_y = this.y;
@ -188,10 +188,10 @@ class OnCanvasTextBox extends OnCanvasObject {
this.height = old_height;
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_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1,
get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
});
let mox, moy; // mouse offset
const pointermove = e => {
@ -234,7 +234,7 @@ class OnCanvasTextBox extends OnCanvasObject {
OnCanvasTextBox.$fontbox = null;
}
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
const fb_rect = $fb[0].getBoundingClientRect();
const tb_rect = this.$el[0].getBoundingClientRect();
@ -262,7 +262,7 @@ class OnCanvasTextBox extends OnCanvasObject {
// 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
// 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' });
displace_font_box();
});

View File

@ -25,7 +25,7 @@ function get_hotkey(text) {
}
let localizations = {};
window.localize = (english_text, ...interpolations)=> {
window.localize = (english_text, ...interpolations) => {
function find_localization(english_text) {
const amp_index = index_of_hotkey(english_text);
if (amp_index > -1) {
@ -51,7 +51,7 @@ window.localize = (english_text, ...interpolations)=> {
}
function interpolate(text, interpolations) {
for (let i = 0; i < interpolations.length; i++) {
text = text.replace(`%${i+1}`, interpolations[i]);
text = text.replace(`%${i + 1}`, interpolations[i]);
}
return text;
}
@ -1038,7 +1038,7 @@ for (const accepted_language of accepted_languages) {
function get_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";
}
function load_language(language) {
@ -1059,8 +1059,8 @@ function load_language(language) {
stylesheet.setAttribute("href", href);
// hack to wait for stylesheet to load
const img = document.createElement("img");
img.onerror = ()=> {
$(()=> {
img.onerror = () => {
$(() => {
$G.triggerHandler("theme-load"); // signal layout change
});
};

View File

@ -133,7 +133,7 @@ window.systemHooks = window.systemHooks || {};
window.systemHookDefaults = {
// named to be distinct from various platform APIs (showSaveFilePicker, saveAs, electron's showSaveDialog; and saveFile is too ambiguous)
// could call it saveFileAs maybe but then it'd be weird that you don't pass in the file directly
showSaveFileDialog: async ({formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle})=> {
showSaveFileDialog: async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle }) => {
// Note: showSaveFilePicker currently doesn't support suggesting a filename,
// or retrieving which file type was selected in the dialog (you have to get it (guess it) from the file name)
// In particular, some formats are ambiguous with the file name, e.g. different bit depths of BMP files.
@ -150,8 +150,8 @@ window.systemHookDefaults = {
// so limit formats shown to a set that can all be used by their unique file extensions
// formats = formats_unique_per_file_extension(formats);
// OR, show two dialogs, one for the format and then one for the save location.
const {newFileFormatID} = await save_as_prompt({dialogTitle, defaultFileName, defaultFileFormatID, formats, promptForName: false});
const new_format = formats.find((format)=> format.formatID === newFileFormatID);
const { newFileFormatID } = await save_as_prompt({ dialogTitle, defaultFileName, defaultFileFormatID, formats, promptForName: false });
const new_format = formats.find((format) => format.formatID === newFileFormatID);
const blob = await getBlob(new_format && new_format.formatID);
formats = [new_format];
let newHandle;
@ -255,7 +255,7 @@ window.systemHookDefaults = {
newBlob: blob,
});
} else {
const {newFileName, newFileFormatID} = await save_as_prompt({dialogTitle, defaultFileName, defaultFileFormatID, formats});
const { newFileName, newFileFormatID } = await save_as_prompt({ dialogTitle, defaultFileName, defaultFileFormatID, formats });
const blob = await getBlob(newFileFormatID);
saveAs(blob, newFileName);
savedCallbackUnreliable && savedCallbackUnreliable({
@ -274,23 +274,23 @@ window.systemHookDefaults = {
}
if (window.showOpenFilePicker && enable_fs_access_api) {
const [fileHandle] = await window.showOpenFilePicker({
types: formats.map((format)=> {
types: formats.map((format) => {
return {
description: format.name,
accept: {
[format.mimeType]: format.extensions.map((extension)=> "." + extension)
[format.mimeType]: format.extensions.map((extension) => "." + extension)
}
}
})
});
const file = await fileHandle.getFile();
return {file, fileHandle};
return { file, fileHandle };
} else {
// @TODO: specify mime types?
return new Promise((resolve)=> {
return new Promise((resolve) => {
const $input = $("<input type='file'>")
.on("change", ()=> {
resolve({file: $input[0].files[0]});
.on("change", () => {
resolve({ file: $input[0].files[0] });
$input.remove();
})
.appendTo($app)
@ -340,7 +340,7 @@ window.systemHookDefaults = {
// show_error_message(`${localize("Failed to open document.")}\n${localize("An unsupported operation was attempted.")}`, error);
}
},
setWallpaperTiled: (canvas)=> {
setWallpaperTiled: (canvas) => {
const wallpaperCanvas = make_canvas(screen.width, screen.height);
const pattern = wallpaperCanvas.ctx.createPattern(canvas, "repeat");
wallpaperCanvas.ctx.fillStyle = pattern;
@ -348,15 +348,15 @@ window.systemHookDefaults = {
systemHooks.setWallpaperCentered(wallpaperCanvas);
},
setWallpaperCentered: (canvas)=> {
setWallpaperCentered: (canvas) => {
systemHooks.showSaveFileDialog({
dialogTitle: localize("Save As"),
defaultName: `${file_name.replace(/\.(bmp|dib|a?png|gif|jpe?g|jpe|jfif|tiff?|webp|raw)$/i, "")} wallpaper.png`,
defaultFileFormatID: "image/png",
formats: image_formats,
getBlob: (new_file_type)=> {
return new Promise((resolve)=> {
write_image_file(canvas, new_file_type, (blob)=> {
getBlob: (new_file_type) => {
return new Promise((resolve) => {
write_image_file(canvas, new_file_type, (blob) => {
resolve(blob);
});
});
@ -387,7 +387,7 @@ function get_format_from_extension(formats, file_path_or_name_or_ext) {
const image_formats = [];
// const ext_to_image_formats = {}; // there can be multiple with the same extension, e.g. different bit depth BMP files
// const mime_type_to_image_formats = {};
const add_image_format = (mime_type, name_and_exts, target_array=image_formats)=> {
const add_image_format = (mime_type, name_and_exts, target_array = image_formats) => {
// Note: some localizations have commas instead of semicolons to separate file extensions
// Assumption: file extensions are never localized
const format = {
@ -432,18 +432,18 @@ add_image_format("image/bmp", "24-bit Bitmap (*.bmp;*.dib)");
// Only support 24bpp BMP files for File System Access API and Electron save dialog,
// as these APIs don't allow you to access the selected file type.
// You can only guess it from the file extension the user types.
const formats_unique_per_file_extension = (formats)=> {
const formats_unique_per_file_extension = (formats) => {
// first handle BMP format specifically to make sure the 24-bpp is the selected BMP format
formats = formats.filter((format)=>
formats = formats.filter((format) =>
format.extensions.includes("bmp") ? format.mimeType === "image/bmp" : true
)
// then generally uniquify on extensions
// (this could be overzealous in case of partial overlap in extensions of different formats,
// but in general it needs special care anyways, to decide which format should win)
// This can't be simply chained with the above because it needs to use the intermediate, partially filtered formats array.
return formats.filter((format, format_index)=>
!format.extensions.some((extension)=>
formats.some((other_format, other_format_index)=>
return formats.filter((format, format_index) =>
!format.extensions.some((extension) =>
formats.some((other_format, other_format_index) =>
other_format_index < format_index &&
other_format.extensions.includes(extension)
)
@ -487,14 +487,13 @@ for (const [format_id, format] of Object.entries(AnyPalette.formats)) {
palette_formats.push({
formatID: format_id,
name: format.name,
nameWithExtensions: `${format.name} (${
format.fileExtensions.map((extension)=> `*.${extension}`).join(";")
nameWithExtensions: `${format.name} (${format.fileExtensions.map((extension) => `*.${extension}`).join(";")
})`,
extensions: format.fileExtensions,
});
}
}
palette_formats.sort((a, b)=>
palette_formats.sort((a, b) =>
// Order important formats first, starting with RIFF PAL format:
(b.formatID === "RIFF_PALETTE") - (a.formatID === "RIFF_PALETTE") ||
(b.formatID === "GIMP_PALETTE") - (a.formatID === "GIMP_PALETTE") ||
@ -551,7 +550,7 @@ let text_tool_font = {
background: "",
};
let root_history_node = make_history_node({name: "App Not Loaded Properly - Please send a bug report."}); // will be replaced
let root_history_node = make_history_node({ name: "App Not Loaded Properly - Please send a bug report." }); // will be replaced
let current_history_node = root_history_node;
let history_node_to_cancel_to = null;
/** array of history nodes */
@ -581,7 +580,7 @@ let update_helper_layer_on_pointermove_active = false;
/** works in client coordinates */
let pointers = [];
const update_from_url_params = ()=> {
const update_from_url_params = () => {
if (location.hash.match(/eye-gaze-mode/i)) {
if (!$("body").hasClass("eye-gaze-mode")) {
$("body").addClass("eye-gaze-mode");
@ -656,11 +655,11 @@ $G.on("hashchange popstate change-url-params", update_from_url_params);
// handle backwards compatibility URLs
if (location.search.match(/eye-gaze-mode/)) {
change_url_param("eye-gaze-mode", true, {replace_history_state: true});
change_url_param("eye-gaze-mode", true, { replace_history_state: true });
update_from_url_params();
}
if (location.search.match(/vertical-colors?-box/)) {
change_url_param("vertical-color-box", true, {replace_history_state: true});
change_url_param("vertical-color-box", true, { replace_history_state: true });
update_from_url_params();
}
@ -677,13 +676,13 @@ let canvas_bounding_client_rect = main_canvas.getBoundingClientRect(); // cached
const canvas_handles = new Handles({
$handles_container: $canvas_area,
$object_container: $canvas_area,
get_rect: ()=> ({x: 0, y: 0, width: main_canvas.width, height: main_canvas.height}),
set_rect: ({width, height})=> resize_canvas_and_save_dimensions(width, height),
get_rect: () => ({ x: 0, y: 0, width: main_canvas.width, height: main_canvas.height }),
set_rect: ({ width, height }) => resize_canvas_and_save_dimensions(width, height),
outset: 4,
get_handles_offset_left: ()=> parseFloat($canvas_area.css("padding-left")) + 1,
get_handles_offset_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1,
get_ghost_offset_left: ()=> parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1,
get_handles_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_handles_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
size_only: true,
});
@ -714,14 +713,14 @@ const $news_indicator = $(`
</span>
</a>
`);
$news_indicator.on("click auxclick", (event)=> {
$news_indicator.on("click auxclick", (event) => {
event.preventDefault();
show_news();
$news_indicator.remove();
try {
localStorage[news_seen_key] = latest_news_datetime;
// eslint-disable-next-line no-empty
} catch (error) {}
} catch (error) { }
});
let news_seen;
let local_storage_unavailable;
@ -733,7 +732,7 @@ try {
const news_period_if_can_dismiss = 15;
const news_period_if_cannot_dismiss = 5;
const news_period = local_storage_unavailable ? news_period_if_cannot_dismiss : news_period_if_can_dismiss;
if (Date.now() < Date.parse(latest_news_datetime) + news_period * 24*60*60*1000 && news_seen !== latest_news_datetime) {
if (Date.now() < Date.parse(latest_news_datetime) + news_period * 24 * 60 * 60 * 1000 && news_seen !== latest_news_datetime) {
$status_area.append($news_indicator);
}
@ -744,26 +743,26 @@ $status_text.default();
// menu bar
let menu_bar_outside_frame = false;
if(frameElement){
try{
if(parent.MenuBar){
if (frameElement) {
try {
if (parent.MenuBar) {
MenuBar = parent.MenuBar;
menu_bar_outside_frame = true;
}
// eslint-disable-next-line no-empty
}catch(e){}
} catch (e) { }
}
const menu_bar = MenuBar(menus);
if(menu_bar_outside_frame){
if (menu_bar_outside_frame) {
$(menu_bar.element).insertBefore(frameElement);
}else{
} else {
$(menu_bar.element).prependTo($V);
}
$(menu_bar.element).on("info", (event) => {
$status_text.text(event.detail?.description ?? "");
});
$(menu_bar.element).on("default-info", ()=> {
$(menu_bar.element).on("default-info", () => {
$status_text.default();
});
// </menu bar>
@ -776,12 +775,12 @@ let $toolbox = $ToolBox(tools);
let $colorbox = $ColorBox($("body").hasClass("vertical-color-box-mode"));
$G.on("vertical-color-box-mode-toggled", ()=> {
$G.on("vertical-color-box-mode-toggled", () => {
$colorbox.destroy();
$colorbox = $ColorBox($("body").hasClass("vertical-color-box-mode"));
prevent_selection($colorbox);
});
$G.on("eye-gaze-mode-toggled", ()=> {
$G.on("eye-gaze-mode-toggled", () => {
$colorbox.destroy();
$colorbox = $ColorBox($("body").hasClass("vertical-color-box-mode"));
prevent_selection($colorbox);
@ -894,7 +893,7 @@ $("body").on("dragover dragenter", (event) => {
});
$G.on("keydown", e => {
if(e.isDefaultPrevented()){
if (e.isDefaultPrevented()) {
return;
}
if (e.key === "Escape") { // Note: Escape handled below too! (after input/textarea return condition)
@ -918,10 +917,10 @@ $G.on("keydown", e => {
// maybe it should only handle the event if document.activeElement is the body or html element?
// (or $app could have a tabIndex and no focus style and be focused under various conditions,
// if that turned out to make more sense for some reason)
if(
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
){
) {
return;
}
@ -929,13 +928,13 @@ $G.on("keydown", e => {
// also, ideally check that modifiers *aren't* pressed
// probably best to use a library at this point!
if(selection){
if (selection) {
const nudge_selection = (delta_x, delta_y) => {
selection.x += delta_x;
selection.y += delta_y;
selection.position();
};
switch(e.key){
switch (e.key) {
case "ArrowLeft":
nudge_selection(-1, 0);
e.preventDefault();
@ -964,7 +963,7 @@ $G.on("keydown", e => {
window.stopSimulatingGestures && window.stopSimulatingGestures();
window.trace_and_sketch_stop && window.trace_and_sketch_stop();
} else if (e.key === "Enter") {
if(selection){
if (selection) {
deselect();
}
} else if (e.key === "F1") {
@ -1006,18 +1005,18 @@ $G.on("keydown", e => {
const minus = e.code === "NumpadSubtract" || e.key === "-";
const delta = plus - minus; // const delta = +plus++ -minus--; // Δ = ±±±±
if(selection){
if (selection) {
selection.scale(2 ** delta);
}else{
if(selected_tool.id === TOOL_BRUSH){
} else {
if (selected_tool.id === TOOL_BRUSH) {
brush_size = Math.max(1, Math.min(brush_size + delta, 500));
}else if(selected_tool.id === TOOL_ERASER){
} else if (selected_tool.id === TOOL_ERASER) {
eraser_size = Math.max(1, Math.min(eraser_size + delta, 500));
}else if(selected_tool.id === TOOL_AIRBRUSH){
} else if (selected_tool.id === TOOL_AIRBRUSH) {
airbrush_size = Math.max(1, Math.min(airbrush_size + delta, 500));
}else if(selected_tool.id === TOOL_PENCIL){
} else if (selected_tool.id === TOOL_PENCIL) {
pencil_size = Math.max(1, Math.min(pencil_size + delta, 50));
}else if(
} else if (
selected_tool.id === TOOL_LINE ||
selected_tool.id === TOOL_CURVE ||
selected_tool.id === TOOL_RECTANGLE ||
@ -1029,8 +1028,8 @@ $G.on("keydown", e => {
}
$G.trigger("option-changed");
if(button !== undefined && pointer){ // pointer may only be needed for tests
selected_tools.forEach((selected_tool)=> {
if (button !== undefined && pointer) { // pointer may only be needed for tests
selected_tools.forEach((selected_tool) => {
tool_go(selected_tool);
});
}
@ -1038,9 +1037,9 @@ $G.on("keydown", e => {
}
e.preventDefault();
return;
}else if(e.ctrlKey || e.metaKey){
if(textbox){
switch(e.key.toUpperCase()){
} else if (e.ctrlKey || e.metaKey) {
if (textbox) {
switch (e.key.toUpperCase()) {
case "A":
case "Z":
case "Y":
@ -1069,14 +1068,14 @@ $G.on("keydown", e => {
case "<":
case "[":
case "{":
rotate(-TAU/4);
rotate(-TAU / 4);
$canvas_area.trigger("resize");
break;
case ".": // '>' without Shift
case ">":
case "]":
case "}":
rotate(+TAU/4);
rotate(+TAU / 4);
$canvas_area.trigger("resize");
break;
case "Z":
@ -1184,31 +1183,31 @@ addEventListener("wheel", (e) => {
}, { passive: false });
$G.on("cut copy paste", e => {
if(e.isDefaultPrevented()){
if (e.isDefaultPrevented()) {
return;
}
if(
if (
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement ||
!window.getSelection().isCollapsed
){
) {
// Don't prevent cutting/copying/pasting within inputs or textareas, or if there's a selection
return;
}
e.preventDefault();
const cd = e.originalEvent.clipboardData || window.clipboardData;
if(!cd){ return; }
if (!cd) { return; }
if(e.type === "copy" || e.type === "cut"){
if(selection && selection.canvas){
if (e.type === "copy" || e.type === "cut") {
if (selection && selection.canvas) {
const do_sync_clipboard_copy_or_cut = () => {
// works only for pasting within a jspaint instance
const data_url = selection.canvas.toDataURL();
cd.setData("text/x-data-uri; type=image/png", data_url);
cd.setData("text/uri-list", data_url);
cd.setData("URL", data_url);
if(e.type === "cut"){
if (e.type === "cut") {
delete_selection({
name: localize("Cut"),
icon: get_help_folder_icon("p_cut.png"),
@ -1224,13 +1223,13 @@ $G.on("cut copy paste", e => {
} else {
edit_copy();
}
} catch(e) {
} catch (e) {
do_sync_clipboard_copy_or_cut();
}
}
}else if(e.type === "paste"){
} else if (e.type === "paste") {
for (const item of cd.items) {
if(item.type.match(/^text\/(?:x-data-uri|uri-list|plain)|URL$/)){
if (item.type.match(/^text\/(?:x-data-uri|uri-list|plain)|URL$/)) {
item.getAsString(text => {
const uris = get_uris(text);
if (uris.length > 0) {
@ -1244,7 +1243,7 @@ $G.on("cut copy paste", e => {
}
});
break;
}else if(item.type.match(/^image\//)){
} else if (item.type.match(/^image\//)) {
paste_image_from_file(item.getAsFile());
break;
}
@ -1263,19 +1262,19 @@ storage.get({
width: default_canvas_width,
height: default_canvas_height,
}, (err, stored_values) => {
if(err){return;}
if (err) { return; }
my_canvas_width = stored_values.width;
my_canvas_height = stored_values.height;
make_or_update_undoable({
match: (history_node)=> history_node.name === localize("New"),
match: (history_node) => history_node.name === localize("New"),
name: "Resize Canvas For New Document",
icon: get_help_folder_icon("p_stretch_both.png"),
}, ()=> {
}, () => {
main_canvas.width = Math.max(1, my_canvas_width);
main_canvas.height = Math.max(1, my_canvas_height);
main_ctx.disable_image_smoothing();
if(!transparency){
if (!transparency) {
main_ctx.fillStyle = selected_colors.background;
main_ctx.fillRect(0, 0, main_canvas.width, main_canvas.height);
}
@ -1294,24 +1293,20 @@ if (window.initial_system_file_handle) {
});
}
const lerp = (a, b, b_ness)=> a + (b - a) * b_ness;
const lerp = (a, b, b_ness) => a + (b - a) * b_ness;
const color_ramp = (num_colors, start_hsla, end_hsla)=>
Array(num_colors).fill().map((_undefined, index, array)=>
`hsla(${
lerp(start_hsla[0], end_hsla[0], index/array.length)
}deg, ${
lerp(start_hsla[1], end_hsla[1], index/array.length)
}%, ${
lerp(start_hsla[2], end_hsla[2], index/array.length)
}%, ${
lerp(start_hsla[3], end_hsla[3], index/array.length)
const color_ramp = (num_colors, start_hsla, end_hsla) =>
Array(num_colors).fill().map((_undefined, index, array) =>
`hsla(${lerp(start_hsla[0], end_hsla[0], index / array.length)
}deg, ${lerp(start_hsla[1], end_hsla[1], index / array.length)
}%, ${lerp(start_hsla[2], end_hsla[2], index / array.length)
}%, ${lerp(start_hsla[3], end_hsla[3], index / array.length)
}%)`
);
const update_palette_from_theme = ()=> {
const update_palette_from_theme = () => {
if (get_theme() === "winter.css") {
const make_stripe_patterns = (reverse)=> [
const make_stripe_patterns = (reverse) => [
make_stripe_pattern(reverse, [
"hsl(166, 93%, 38%)",
"white",
@ -1395,7 +1390,7 @@ const update_palette_from_theme = ()=> {
$G.on("theme-load", update_palette_from_theme);
update_palette_from_theme();
function to_canvas_coords({clientX, clientY}) {
function to_canvas_coords({ clientX, clientY }) {
if (clientX === undefined || clientY === undefined) {
throw new TypeError("clientX and clientY must be defined (not {x, y} or x, y or [x, y])");
}
@ -1405,7 +1400,7 @@ function to_canvas_coords({clientX, clientY}) {
y: ~~((clientY - rect.top) / rect.height * main_canvas.height),
};
}
function from_canvas_coords({x, y}) {
function from_canvas_coords({ x, y }) {
const rect = canvas_bounding_client_rect;
return {
clientX: ~~(x / main_canvas.width * rect.width + rect.left),
@ -1428,12 +1423,12 @@ function update_fill_and_stroke_colors_and_lineWidth(selected_tool) {
stroke_color_k =
ctrl ? "ternary" : ((reverse ^ reverse_because_fill_only) ? "background" : "foreground");
if(selected_tool.shape || selected_tool.shape_colors){
if(!selected_tool.stroke_only){
if((reverse ^ reverse_because_fill_only)){
if (selected_tool.shape || selected_tool.shape_colors) {
if (!selected_tool.stroke_only) {
if ((reverse ^ reverse_because_fill_only)) {
fill_color_k = "foreground";
stroke_color_k = "background";
}else{
} else {
fill_color_k = "background";
stroke_color_k = "foreground";
}
@ -1443,17 +1438,17 @@ function update_fill_and_stroke_colors_and_lineWidth(selected_tool) {
}
}
function tool_go(selected_tool, event_name){
function tool_go(selected_tool, event_name) {
update_fill_and_stroke_colors_and_lineWidth(selected_tool);
if(selected_tool[event_name]){
if (selected_tool[event_name]) {
selected_tool[event_name](main_ctx, pointer.x, pointer.y);
}
if(selected_tool.paint){
if (selected_tool.paint) {
selected_tool.paint(main_ctx, pointer.x, pointer.y);
}
}
function canvas_pointer_move(e){
function canvas_pointer_move(e) {
ctrl = e.ctrlKey;
shift = e.shiftKey;
pointer = to_canvas_coords(e);
@ -1461,18 +1456,18 @@ function canvas_pointer_move(e){
// Quick Undo (for mouse/pen)
// (Note: pointermove also occurs when the set of buttons pressed changes,
// except when another event would fire like pointerdown)
if(pointers.length && e.button != -1){
if (pointers.length && e.button != -1) {
// compare buttons other than middle mouse button by using bitwise OR to make that bit of the number the same
const MMB = 4;
if(e.pointerType != pointer_type || (e.buttons | MMB) != (pointer_buttons | MMB)){
if (e.pointerType != pointer_type || (e.buttons | MMB) != (pointer_buttons | MMB)) {
cancel();
pointer_active = false; // NOTE: pointer_active used in cancel()
return;
}
}
if(e.shiftKey){
if(
if (e.shiftKey) {
if (
selected_tool.id === TOOL_LINE ||
selected_tool.id === TOOL_CURVE
) {
@ -1486,26 +1481,26 @@ function canvas_pointer_move(e){
const angle = Math.round(angle_0_to_8) * eighth_turn;
pointer.x = Math.round(pointer_start.x + Math.cos(angle) * dist);
pointer.y = Math.round(pointer_start.y + Math.sin(angle) * dist);
}else if(selected_tool.shape){
} else if (selected_tool.shape) {
// snap to four diagonals
const w = Math.abs(pointer.x - pointer_start.x);
const h = Math.abs(pointer.y - pointer_start.y);
if(w < h){
if(pointer.y > pointer_start.y){
if (w < h) {
if (pointer.y > pointer_start.y) {
pointer.y = pointer_start.y + w;
}else{
} else {
pointer.y = pointer_start.y - w;
}
}else{
if(pointer.x > pointer_start.x){
} else {
if (pointer.x > pointer_start.x) {
pointer.x = pointer_start.x + h;
}else{
} else {
pointer.x = pointer_start.x - h;
}
}
}
}
selected_tools.forEach((selected_tool)=> {
selected_tools.forEach((selected_tool) => {
tool_go(selected_tool);
});
pointer_previous = pointer;
@ -1514,7 +1509,7 @@ $canvas.on("pointermove", e => {
pointer = to_canvas_coords(e);
$status_position.text(`${pointer.x},${pointer.y}`);
});
$canvas.on("pointerenter", (e)=> {
$canvas.on("pointerenter", (e) => {
pointer_over_canvas = true;
update_helper_layer(e);
@ -1524,7 +1519,7 @@ $canvas.on("pointerenter", (e)=> {
update_helper_layer_on_pointermove_active = true;
}
});
$canvas.on("pointerleave", (e)=> {
$canvas.on("pointerleave", (e) => {
pointer_over_canvas = false;
$status_position.text("");
@ -1537,8 +1532,8 @@ $canvas.on("pointerleave", (e)=> {
}
});
let clean_up_eye_gaze_mode = ()=> {};
$G.on("eye-gaze-mode-toggled", ()=> {
let clean_up_eye_gaze_mode = () => { };
$G.on("eye-gaze-mode-toggled", () => {
if ($("body").hasClass("eye-gaze-mode")) {
init_eye_gaze_mode();
} else {
@ -1605,7 +1600,7 @@ const eye_gaze_mode_config = {
dwellClickEvenIfPaused: (target) => (
target.matches(".toggle-dwell-clicking")
),
shouldDrag: (target)=> (
shouldDrag: (target) => (
target.matches(".window-titlebar, .window-titlebar *:not(button)") ||
target.matches(".selection, .selection *, .handle, .grab-region") ||
(
@ -1751,7 +1746,7 @@ async function init_eye_gaze_mode() {
let hover_candidate;
let gaze_dragging = null;
const deactivate_for_at_least = (timespan)=> {
const deactivate_for_at_least = (timespan) => {
inactive_until_time = Math.max(inactive_until_time, Date.now() + timespan);
};
deactivate_for_at_least(inactive_at_startup_timespan);
@ -1767,27 +1762,27 @@ async function init_eye_gaze_mode() {
dwell_indicator.style.display = "none";
document.body.appendChild(dwell_indicator);
const on_pointer_move = (e)=> {
recent_points.push({x: e.clientX, y: e.clientY, time: Date.now()});
const on_pointer_move = (e) => {
recent_points.push({ x: e.clientX, y: e.clientY, time: Date.now() });
};
const on_pointer_up_or_cancel = (e)=> {
const on_pointer_up_or_cancel = (e) => {
deactivate_for_at_least(inactive_after_release_timespan);
gaze_dragging = null;
};
let page_focused = document.visibilityState === "visible"; // guess/assumption
let mouse_inside_page = true; // assumption
const on_focus = ()=> {
const on_focus = () => {
page_focused = true;
deactivate_for_at_least(inactive_after_focused_timespan);
};
const on_blur = ()=> {
const on_blur = () => {
page_focused = false;
};
const on_mouse_leave_page = ()=> {
const on_mouse_leave_page = () => {
mouse_inside_page = false;
};
const on_mouse_enter_page = ()=> {
const on_mouse_enter_page = () => {
mouse_inside_page = true;
};
@ -1799,7 +1794,7 @@ async function init_eye_gaze_mode() {
document.addEventListener("mouseleave", on_mouse_leave_page);
document.addEventListener("mouseenter", on_mouse_enter_page);
const get_hover_candidate = (clientX, clientY)=> {
const get_hover_candidate = (clientX, clientY) => {
if (!page_focused || !mouse_inside_page) return null;
@ -1815,7 +1810,7 @@ async function init_eye_gaze_mode() {
};
let retargeted = false;
for (const {from, to, withinMargin=Infinity} of eye_gaze_mode_config.retarget) {
for (const { from, to, withinMargin = Infinity } of eye_gaze_mode_config.retarget) {
if (
from instanceof Element ? from === target :
typeof from === "function" ? from(target) :
@ -1874,7 +1869,7 @@ async function init_eye_gaze_mode() {
return hover_candidate;
};
const get_event_options = ({x, y})=> {
const get_event_options = ({ x, y }) => {
return {
view: window, // needed for offsetX/Y calculation
clientX: x,
@ -1887,12 +1882,12 @@ async function init_eye_gaze_mode() {
};
};
const update = ()=> {
const update = () => {
const time = Date.now();
recent_points = recent_points.filter((point_record)=> time < point_record.time + averaging_window_timespan);
recent_points = recent_points.filter((point_record) => time < point_record.time + averaging_window_timespan);
if (recent_points.length) {
const latest_point = recent_points[recent_points.length - 1];
recent_points.push({x: latest_point.x, y: latest_point.y, time});
recent_points.push({ x: latest_point.x, y: latest_point.y, time });
const average_point = average_points(recent_points);
// debug
// const canvas_point = to_canvas_coords({clientX: average_point.x, clientY: average_point.y});
@ -1908,7 +1903,7 @@ async function init_eye_gaze_mode() {
// (but TODO: just move the indicator off center in that case)
if (hover_candidate && !gaze_dragging) {
const apparent_hover_candidate = get_hover_candidate(hover_candidate.x, hover_candidate.y);
const show_occluder_indicator = (occluder)=> {
const show_occluder_indicator = (occluder) => {
const occluder_indicator = document.createElement("div");
const occluder_rect = occluder.getBoundingClientRect();
const outline_width = 4;
@ -2003,8 +1998,8 @@ async function init_eye_gaze_mode() {
dwell_indicator.style.display = "";
dwell_indicator.style.opacity = circle_opacity;
dwell_indicator.style.transform = `scale(${circle_radius / circle_radius_max})`;
dwell_indicator.style.left = `${circle_position.x - circle_radius_max/2}px`;
dwell_indicator.style.top = `${circle_position.y - circle_radius_max/2}px`;
dwell_indicator.style.left = `${circle_position.x - circle_radius_max / 2}px`;
dwell_indicator.style.top = `${circle_position.y - circle_radius_max / 2}px`;
let halo_target =
gaze_dragging ||
@ -2093,7 +2088,7 @@ async function init_eye_gaze_mode() {
}
};
let raf_id;
const animate = ()=> {
const animate = () => {
raf_id = requestAnimationFrame(animate);
update();
};
@ -2116,7 +2111,7 @@ async function init_eye_gaze_mode() {
const $pause_button = $(`<button class="toggle-dwell-clicking"/>`)
.attr("title", pause_button_text)
.on("click", ()=> {
.on("click", () => {
paused = !paused;
$("body").toggleClass("eye-gaze-mode-paused", paused);
$pause_button.attr("title", paused ? resume_button_text : pause_button_text);
@ -2126,7 +2121,7 @@ async function init_eye_gaze_mode() {
$("<div class='button-icon'>")
);
clean_up_eye_gaze_mode = ()=> {
clean_up_eye_gaze_mode = () => {
console.log("Cleaning up / disabling eye gaze mode");
cancelAnimationFrame(raf_id);
halo.remove();
@ -2139,7 +2134,7 @@ async function init_eye_gaze_mode() {
window.removeEventListener("blur", on_blur);
document.removeEventListener("mouseleave", on_mouse_leave_page);
document.removeEventListener("mouseenter", on_mouse_enter_page);
clean_up_eye_gaze_mode = ()=> {};
clean_up_eye_gaze_mode = () => { };
};
}
@ -2149,7 +2144,7 @@ let pan_start_magnification; // for panning and zooming in the same gesture
let first_pointer_time;
const discard_quick_undo_period = 500; // milliseconds in which to treat gesture as just a pan/zoom if you use two fingers, rather than treating it as a brush stroke you might care about
function average_points(points) {
const average = {x: 0, y: 0};
const average = { x: 0, y: 0 };
for (const pointer of points) {
average.x += pointer.x;
average.y += pointer.y;
@ -2158,13 +2153,13 @@ function average_points(points) {
average.y /= points.length;
return average;
}
$canvas_area.on("pointerdown", (event)=> {
$canvas_area.on("pointerdown", (event) => {
if (document.activeElement && document.activeElement !== document.body && document.activeElement !== document.documentElement) {
// Allow unfocusing dialogs etc. in order to use keyboard shortcuts
document.activeElement.blur();
}
if (pointers.every((pointer)=>
if (pointers.every((pointer) =>
// prevent multitouch panning in case of synthetic events from eye gaze mode
pointer.pointerId !== 1234567890 &&
// prevent multitouch panning in case of dragging across iframe boundary with a mouse/pen
@ -2201,12 +2196,12 @@ $canvas_area.on("pointerdown", (event)=> {
return;
}
});
$G.on("pointerup pointercancel", (event)=> {
pointers = pointers.filter((pointer)=>
$G.on("pointerup pointercancel", (event) => {
pointers = pointers.filter((pointer) =>
pointer.pointerId !== event.pointerId
);
});
$G.on("pointermove", (event)=> {
$G.on("pointermove", (event) => {
for (const pointer of pointers) {
if (pointer.pointerId === event.pointerId) {
pointer.x = event.clientX;
@ -2248,7 +2243,7 @@ $canvas.on("pointerdown", e => {
// SEE OTHER POINTERDOWN HANDLER ALSO
// NOTE: this relies on event handler order for pointerdown
// pointer is not added to pointers yet
if(pointers.length >= 1){
if (pointers.length >= 1) {
// If you press two fingers quickly, it shouldn't make a new history entry.
// But if you draw something and then press a second finger to clear it, it should let you redo.
const discard_document_state = first_pointer_time && performance.now() - first_pointer_time < discard_quick_undo_period;
@ -2256,7 +2251,7 @@ $canvas.on("pointerdown", e => {
pointer_active = false; // NOTE: pointer_active used in cancel(); must be set after cancel()
// in eye gaze mode, allow drawing with mouse after canceling gaze gesture with mouse
pointers = pointers.filter((pointer)=>
pointers = pointers.filter((pointer) =>
pointer.pointerId !== 1234567890
);
return;
@ -2267,7 +2262,7 @@ $canvas.on("pointerdown", e => {
pointer_active = !!(e.buttons & (1 | 2)); // as far as tools are concerned
pointer_type = e.pointerType;
pointer_buttons = e.buttons;
$G.one("pointerup", (e)=> {
$G.one("pointerup", (e) => {
pointer_active = false;
update_helper_layer(e);
@ -2277,11 +2272,11 @@ $canvas.on("pointerdown", e => {
}
});
if(e.button === 0){
if (e.button === 0) {
reverse = false;
}else if(e.button === 2){
} else if (e.button === 2) {
reverse = true;
}else{
} else {
return;
}
@ -2292,12 +2287,12 @@ $canvas.on("pointerdown", e => {
const pointerdown_action = () => {
let interval_ids = [];
selected_tools.forEach((selected_tool)=> {
if(selected_tool.paint || selected_tool.pointerdown){
selected_tools.forEach((selected_tool) => {
if (selected_tool.paint || selected_tool.pointerdown) {
tool_go(selected_tool, "pointerdown");
}
if(selected_tool.paint_on_time_interval != null){
interval_ids.push(setInterval(()=> {
if (selected_tool.paint_on_time_interval != null) {
interval_ids.push(setInterval(() => {
tool_go(selected_tool);
}, selected_tool.paint_on_time_interval));
}
@ -2342,9 +2337,9 @@ $canvas.on("pointerdown", e => {
});
$canvas_area.on("pointerdown", e => {
if(e.button === 0){
if($canvas_area.is(e.target)){
if(selection){
if (e.button === 0) {
if ($canvas_area.is(e.target)) {
if (selection) {
deselect();
}
}
@ -2353,18 +2348,18 @@ $canvas_area.on("pointerdown", e => {
function prevent_selection($el) {
$el.on("mousedown selectstart contextmenu", (e) => {
if(e.isDefaultPrevented()){
if (e.isDefaultPrevented()) {
return;
}
if(
if (
e.target instanceof HTMLSelectElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target instanceof HTMLLabelElement && e.type !== "contextmenu") ||
(e.target instanceof HTMLInputElement && e.target.type !== "color")
){
) {
return;
}
if(e.button === 1){
if (e.button === 1) {
return; // allow middle-click scrolling
}
e.preventDefault();

View File

@ -34,7 +34,7 @@ try {
// eslint-disable-next-line no-empty
} catch (error) { }
if (dev_edit_colors) {
$(()=> {
$(() => {
show_edit_colors_window();
$(".define-custom-colors-button").click();
$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 swatch_index = $palette.find(".swatch").toArray().indexOf($swatch_to_edit[0]);
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,
// toggling vertical color box mode, or monochrome document mode.
$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_matches_main_canvas_colors =
selection_monochrome_info.isMonochrome &&
selection_monochrome_info.presentNonTransparentRGBAs.every((rgba)=>
main_monochrome_info.presentNonTransparentRGBAs.map(rgba=> rgba.toString()).includes(rgba.toString())
selection_monochrome_info.presentNonTransparentRGBAs.every((rgba) =>
main_monochrome_info.presentNonTransparentRGBAs.map(rgba => rgba.toString()).includes(rgba.toString())
);
if (
main_monochrome_info.isMonochrome &&
selection_monochrome_info.isMonochrome &&
selection_matches_main_canvas_colors
) {
const recolor = (ctx, present_rgbas)=> {
const recolor = (ctx, present_rgbas) => {
// HTML5 Canvas API is unreliable for exact colors.
// 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
@ -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.)
if (
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.
const distances = present_rgbas.map((rgba)=>
const distances = present_rgbas.map((rgba) =>
Math.abs(rgba[0] - old_rgba[0]) +
Math.abs(rgba[1] - old_rgba[1]) +
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({
name: "Recolor",
icon: get_help_folder_icon("p_color.png"),
}, ()=> {
}, () => {
recolor(main_ctx, main_monochrome_info.presentNonTransparentRGBAs);
if (selection && selection.canvas) {
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);
selected_colors[color_selection_slot_to_edit] = color;
$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;
const get_current_color = ()=> `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`;
const set_color_from_rgb = (r, g, b)=> {
const get_current_color = () => `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`;
const set_color_from_rgb = (r, g, b) => {
const [h, s, l] = rgb_to_hsl(r, g, b);
hue_degrees = h * 360;
sat_percent = s * 100;
lum_percent = l * 100;
};
const set_color = (color)=> {
const set_color = (color) => {
const [r, g, b] = get_rgba_from_color(color);
set_color_from_rgb(r, g, b);
};
const select = ($swatch)=> {
const select = ($swatch) => {
$w.$content.find(".swatch").removeClass("selected");
$swatch.addClass("selected");
set_color($swatch[0].dataset.color);
@ -183,8 +183,8 @@ function choose_color(initial_color, callback) {
update_inputs("hslrgb");
};
const make_color_grid = (colors, id)=> {
const $color_grid = $(`<div class="color-grid" tabindex="0">`).attr({id});
const make_color_grid = (colors, id) => {
const $color_grid = $(`<div class="color-grid" tabindex="0">`).attr({ id });
for (const color of colors) {
const $swatch = $Swatch(color);
$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");
const num_colors_per_row = 8;
const navigate = (relative_index)=> {
const navigate = (relative_index) => {
const $focused = $color_grid.find(".swatch:focus");
if (!$focused.length) { return; }
const $swatches = $color_grid.find(".swatch");
@ -205,7 +205,7 @@ function choose_color(initial_color, callback) {
if (!$to_focus.length) { return; }
$to_focus.focus();
};
$color_grid.on("keydown", (event)=> {
$color_grid.on("keydown", (event) => {
// console.log(event.code);
if (event.code === "ArrowRight") { navigate(+1); }
if (event.code === "ArrowLeft") { navigate(-1); }
@ -218,17 +218,17 @@ function choose_color(initial_color, callback) {
draw();
}
});
$color_grid.on("pointerdown", (event)=> {
$color_grid.on("pointerdown", (event) => {
const $swatch = $(event.target).closest(".swatch");
if ($swatch.length) {
select($swatch);
draw();
}
});
$color_grid.on("dragstart", (event)=> {
$color_grid.on("dragstart", (event) => {
event.preventDefault();
});
$color_grid.on("focusin", (event)=> {
$color_grid.on("focusin", (event) => {
if (event.target.closest(".swatch")) {
$local_last_focus = $(event.target.closest(".swatch"));
} else {
@ -241,7 +241,7 @@ function choose_color(initial_color, callback) {
// since the parent grid is previous in the tab order
$color_grid.attr("tabindex", -1);
});
$color_grid.on("focusout", (event)=> {
$color_grid.on("focusout", (event) => {
$color_grid.attr("tabindex", 0);
});
return $color_grid;
@ -273,7 +273,7 @@ function choose_color(initial_color, callback) {
const $define_custom_colors_button = $(`<button class="define-custom-colors-button">`)
.html(display_hotkey("&Define Custom Colors >>"))
.appendTo($left)
.on("click", (e)=> {
.on("click", (e) => {
// 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)
e.preventDefault();
@ -290,7 +290,7 @@ function choose_color(initial_color, callback) {
});
// 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 ($w.width() < 300 || document.body.classList.contains("eye-gaze-mode")) {
$define_custom_colors_button.removeAttr("disabled");
@ -318,40 +318,40 @@ function choose_color(initial_color, callback) {
let mouse_down_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) {
// rainbow
for (let y = 0; y < rainbow_canvas.height; y += 6) {
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);
}
}
// crosshair
if (!mouse_down_on_rainbow_canvas) {
const x = ~~(hue_degrees/360*rainbow_canvas.width);
const y = ~~((1-sat_percent/100)*rainbow_canvas.height);
const x = ~~(hue_degrees / 360 * rainbow_canvas.width);
const y = ~~((1 - sat_percent / 100) * rainbow_canvas.height);
rainbow_canvas.ctx.fillStyle = "black";
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-9, y-1, 5, 3);
rainbow_canvas.ctx.fillRect(x+5, y-1, 5, 3);
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 - 9, y - 1, 5, 3);
rainbow_canvas.ctx.fillRect(x + 5, y - 1, 5, 3);
}
crosshair_shown_on_rainbow_canvas = !mouse_down_on_rainbow_canvas;
}
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);
}
lum_arrow_canvas.ctx.fillStyle = getComputedStyle($w.$content[0]).getPropertyValue("--ButtonText");
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.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.fillRect(0, 0, result_canvas.width, result_canvas.height);
@ -361,14 +361,14 @@ function choose_color(initial_color, callback) {
$(luminosity_canvas).addClass("luminosity-canvas inset-shallow");
$(result_canvas).addClass("result-color-canvas inset-shallow").attr("id", "color-solid-canvas");
const select_hue_sat = (event)=> {
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;
const select_hue_sat = (event) => {
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;
update_inputs("hsrgb");
draw();
event.preventDefault();
};
$(rainbow_canvas).on("pointerdown", (event)=> {
$(rainbow_canvas).on("pointerdown", (event) => {
mouse_down_on_rainbow_canvas = true;
select_hue_sat(event);
@ -377,20 +377,20 @@ function choose_color(initial_color, callback) {
rainbow_canvas.setPointerCapture(event.pointerId);
}
});
$G.on("pointerup pointercancel", (event)=> {
$G.on("pointerup pointercancel", (event) => {
$(rainbow_canvas).off("pointermove", select_hue_sat);
// rainbow_canvas.releasePointerCapture(event.pointerId);
mouse_down_on_rainbow_canvas = false;
draw();
});
const select_lum = (event)=> {
lum_percent = Math.min(1, Math.max(0, (1 - event.offsetY/luminosity_canvas.height)))*100;
const select_lum = (event) => {
lum_percent = Math.min(1, Math.max(0, (1 - event.offsetY / luminosity_canvas.height))) * 100;
update_inputs("lrgb");
draw();
event.preventDefault();
};
$(luminosity_canvas).on("pointerdown", (event)=> {
$(luminosity_canvas).on("pointerdown", (event) => {
select_lum(event);
$(luminosity_canvas).on("pointermove", select_lum);
@ -398,15 +398,15 @@ function choose_color(initial_color, callback) {
luminosity_canvas.setPointerCapture(event.pointerId);
}
});
$G.on("pointerup pointercancel", (event)=> {
$G.on("pointerup pointercancel", (event) => {
$(luminosity_canvas).off("pointermove", select_lum);
// luminosity_canvas.releasePointerCapture(event.pointerId);
});
const inputs_by_component_letter = {};
["hsl", "rgb"].forEach((color_model, color_model_index)=> {
[...color_model].forEach((component_letter, component_index)=> {
["hsl", "rgb"].forEach((color_model, color_model_index) => {
[...color_model].forEach((component_letter, component_index) => {
const text_with_hotkey = {
h: "Hu&e:",
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)
$right.on("input", "input", (event)=> {
$right.on("input", "input", (event) => {
const input = event.target;
const component_letter = input.dataset.componentLetter;
if (component_letter) {
@ -489,7 +489,7 @@ function choose_color(initial_color, callback) {
update_inputs("rgb");
} else {
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;
set_color_from_rgb(rgb.r, rgb.g, rgb.b);
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 component_letter = input.dataset.componentLetter;
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) {
switch (event.key) {
case "o":
@ -563,7 +563,7 @@ function choose_color(initial_color, callback) {
event.stopPropagation();
});
const update_inputs = (components)=> {
const update_inputs = (components) => {
for (const component_letter of components) {
const input = inputs_by_component_letter[component_letter];
const [r, g, b] = get_rgba_from_color(get_current_color());
@ -583,7 +583,7 @@ function choose_color(initial_color, callback) {
const $add_to_custom_colors_button = $(`<button class="add-to-custom-colors-button">`)
.html(display_hotkey("&Add To Custom Colors"))
.appendTo($right)
.on("click", (event)=> {
.on("click", (event) => {
// 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)
event.preventDefault();

View File

@ -110,7 +110,7 @@ const createWindow = () => {
// Open links without target=_blank externally.
mainWindow.webContents.on('will-navigate', (e, url) => {
// check that the URL is not part of the app
if(!url.includes("file://")){
if (!url.includes("file://")) {
e.preventDefault();
shell.openExternal(url);
}
@ -118,7 +118,7 @@ const createWindow = () => {
// Open links with target=_blank externally.
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// check that the URL is not part of the app
if(!url.includes("file://")){
if (!url.includes("file://")) {
shell.openExternal(url);
}
return { action: "deny" };

View File

@ -11,11 +11,11 @@ extra_tools = [{
// @XXX: copy pasted all this brush caching/rendering code!
// @TODO: DRY!
const csz = get_brush_canvas_size(brush_size, brush_shape);
if(
if (
this.rendered_shape !== brush_shape ||
this.rendered_color !== stroke_color ||
this.rendered_size !== brush_size
){
) {
brush_canvas.width = csz;
brush_canvas.height = csz;
// don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush
@ -28,14 +28,14 @@ extra_tools = [{
this.rendered_shape = brush_shape;
}
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;
for(let i = 0; i < 6 + r/5; i++){
const rx = (Math.random()*2-1) * r;
const ry = (Math.random()*2-1) * r;
const d = rx*rx + ry*ry;
if(d <= r * r){
for (let i = 0; i < 6 + r / 5; i++) {
const rx = (Math.random() * 2 - 1) * r;
const ry = (Math.random() * 2 - 1) * r;
const d = rx * rx + ry * ry;
if (d <= r * r) {
draw_brush(x + ~~rx, y + ~~ry);
}
}
@ -67,11 +67,11 @@ extra_tools = [{
// @XXX: copy pasted all this brush caching/rendering code!
// @TODO: DRY!
const csz = get_brush_canvas_size(brush_size, brush_shape);
if(
if (
this.rendered_shape !== brush_shape ||
this.rendered_color !== stroke_color ||
this.rendered_size !== brush_size
){
) {
brush_canvas.width = csz;
brush_canvas.height = csz;
// don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush
@ -84,9 +84,9 @@ extra_tools = [{
this.rendered_shape = brush_shape;
}
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 y_diff = y - this.position.y;
const dist = Math.hypot(x_diff, y_diff);

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ function show_help() {
root: "help",
contentsFile: "help/mspaint.hhc",
}).$help_window;
$help_window.on("close", ()=> {
$help_window.on("close", () => {
$help_window = null;
});
}
@ -18,7 +18,7 @@ function show_help() {
// shared code with 98.js.org
// (copy-pasted / manually synced for now)
function open_help_viewer(options){
function open_help_viewer(options) {
const $help_window = $Window({
title: options.title || "Help Topics",
icons: {
@ -34,11 +34,11 @@ function open_help_viewer(options){
const $main = $(E("div")).addClass("main");
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'>")
.append($("<span>").text(name))
.appendTo($toolbar)
.on("click", ()=> {
.on("click", () => {
action_fn();
});
$("<div class='icon'/>")
@ -46,7 +46,7 @@ function open_help_viewer(options){
.css({
backgroundPosition: `${-sprite_n * 55}px 0px`,
});
const update_enabled = ()=> {
const update_enabled = () => {
$button[0].disabled = enabled_fn && !enabled_fn();
};
update_enabled();
@ -54,12 +54,12 @@ function open_help_viewer(options){
$help_window.on("update-buttons", update_enabled);
return $button;
};
const measure_sidebar_width = ()=>
const measure_sidebar_width = () =>
$contents.outerWidth() +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-right")) +
$resizer.outerWidth();
const $hide_button = add_toolbar_button("Hide", 0, ()=> {
const $hide_button = add_toolbar_button("Hide", 0, () => {
const toggling_width = measure_sidebar_width();
$contents.hide();
$resizer.hide();
@ -69,7 +69,7 @@ function open_help_viewer(options){
$help_window.css("left", $help_window.offset().left + toggling_width);
$help_window.bringTitleBarInBounds();
});
const $show_button = add_toolbar_button("Show", 5, ()=> {
const $show_button = add_toolbar_button("Show", 5, () => {
$contents.show();
$resizer.show();
$show_button.hide();
@ -86,31 +86,31 @@ function open_help_viewer(options){
}
$help_window.css("max-width", "");
}).hide();
add_toolbar_button("Back", 1, ()=> {
add_toolbar_button("Back", 1, () => {
$iframe[0].contentWindow.history.back();
ignore_one_load = true;
back_length -= 1;
forward_length += 1;
}, ()=> back_length > 0);
add_toolbar_button("Forward", 2, ()=> {
}, () => back_length > 0);
add_toolbar_button("Forward", 2, () => {
$iframe[0].contentWindow.history.forward();
ignore_one_load = true;
forward_length -= 1;
back_length += 1;
}, ()=> forward_length > 0);
add_toolbar_button("Options", 3, ()=> {}, ()=> false); // @TODO: hotkey and underline on O
add_toolbar_button("Web Help", 4, ()=> {
}, () => forward_length > 0);
add_toolbar_button("Options", 3, () => { }, () => false); // @TODO: hotkey and underline on O
add_toolbar_button("Web Help", 4, () => {
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];
iframe.$window = $help_window; // for focus handling integration
const $resizer = $(E("div")).addClass("resizer");
const $contents = $(E("ul")).addClass("contents inset-deep");
// @TODO: fix race conditions
$iframe.on("load", ()=> {
$iframe.on("load", () => {
if (!ignore_one_load) {
back_length += 1;
forward_length = 0;
@ -123,9 +123,9 @@ function open_help_viewer(options){
$main.append($contents, $resizer, $iframe);
$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({
backgroundColor: "white",
border: "",
@ -152,13 +152,13 @@ function open_help_viewer(options){
bottom: 0,
zIndex: 1,
});
$resizer.on("pointerdown", (e)=> {
$resizer.on("pointerdown", (e) => {
let pointermove, pointerup;
const getPos = (e)=>
const getPos = (e) =>
Math.min($help_window.width() - 100, Math.max(20,
e.clientX - $help_window.$content.offset().left
));
$G.on("pointermove", pointermove = (e)=> {
$G.on("pointermove", pointermove = (e) => {
$resizer.css({
position: "absolute",
left: getPos(e)
@ -167,7 +167,7 @@ function open_help_viewer(options){
marginRight: resizer_width,
});
});
$G.on("pointerup", pointerup = (e)=> {
$G.on("pointerup", pointerup = (e) => {
$G.off("pointermove", pointermove);
$G.off("pointerup", pointerup);
$resizer.css({
@ -200,8 +200,8 @@ function open_help_viewer(options){
});
$item.on("click", () => {
const $li = $item.parent();
if($li.is(".folder")){
if($last_expanded){
if ($li.is(".folder")) {
if ($last_expanded) {
$last_expanded.not($li).removeClass("expanded");
}
$li.toggleClass("expanded");
@ -212,14 +212,14 @@ function open_help_viewer(options){
};
const $default_item_li = $(E("li")).addClass("page");
$default_item_li.append($Item("Welcome to Help").on("click", ()=> {
$iframe.attr({src: "help/default.html"});
$default_item_li.append($Item("Welcome to Help").on("click", () => {
$iframe.attr({ src: "help/default.html" });
}));
$contents.append($default_item_li);
function renderItem(source_li, $folder_items_ul) {
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");
$folder_li.append($Item(object.Name));
@ -228,13 +228,13 @@ function open_help_viewer(options){
const $folder_items_ul = $(E("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);
});
} else {
const $item_li = $(E("li")).addClass("page");
$item_li.append($Item(object.Name).on("click", ()=> {
$iframe.attr({src: `${options.root}/${object.Local}`});
$item_li.append($Item(object.Name).on("click", () => {
$iframe.attr({ src: `${options.root}/${object.Local}` });
}));
if ($folder_items_ul) {
$folder_items_ul.append($item_li);
@ -244,15 +244,15 @@ function open_help_viewer(options){
}
}
fetch(options.contentsFile).then((response)=> {
response.text().then((hhc)=> {
$($.parseHTML(hhc)).filter("ul").children().get().forEach((li)=> {
fetch(options.contentsFile).then((response) => {
response.text().then((hhc) => {
$($.parseHTML(hhc)).filter("ul").children().get().forEach((li) => {
renderItem(li, null);
});
}, (error)=> {
}, (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
if (location.protocol === "file:") {
showMessageBox({
@ -290,13 +290,13 @@ function open_help_viewer(options){
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[0];
var disable_delegate_pointerup = false;
$iframe.focus_contents = function(){
$iframe.focus_contents = function () {
if (!iframe.contentWindow) {
return;
}
@ -306,34 +306,34 @@ function $Iframe(options){
disable_delegate_pointerup = true;
iframe.contentWindow.focus();
setTimeout(function(){
setTimeout(function () {
iframe.contentWindow.focus();
disable_delegate_pointerup = false;
});
};
// Let the iframe to handle mouseup events outside itself
var delegate_pointerup = function(){
var delegate_pointerup = function () {
if (disable_delegate_pointerup) {
return;
}
// This try-catch may only be needed for running Cypress tests.
try {
if(iframe.contentWindow && iframe.contentWindow.jQuery){
if (iframe.contentWindow && iframe.contentWindow.jQuery) {
iframe.contentWindow.jQuery("body").trigger("pointerup");
}
if(iframe.contentWindow){
const event = new iframe.contentWindow.MouseEvent("mouseup", {button: 0});
if (iframe.contentWindow) {
const event = new iframe.contentWindow.MouseEvent("mouseup", { button: 0 });
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);
}
} catch(error) {
} catch (error) {
console.log("Failed to access iframe to delegate pointerup; got", error);
}
};
$G.on("mouseup blur", delegate_pointerup);
$iframe.destroy = ()=> {
$iframe.destroy = () => {
$G.off("mouseup blur", delegate_pointerup);
};
@ -342,9 +342,9 @@ function $Iframe(options){
$("body").addClass("loading-program");
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");
}
@ -374,7 +374,7 @@ function $Iframe(options){
}
var $contentWindow = $(iframe.contentWindow);
$contentWindow.on("pointerdown click", function(e){
$contentWindow.on("pointerdown click", function (e) {
iframe.$window && iframe.$window.focus();
// from close_menus in $MenuBar
@ -383,18 +383,18 @@ function $Iframe(options){
$(".menu-popup").hide();
});
// 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");
$("body").addClass("dragging");
});
$contentWindow.on("pointerup", function(e){
$contentWindow.on("pointerup", function (e) {
$("body").removeClass("dragging");
$iframe.css("pointer-events", "");
});
// $("iframe").css("pointer-events", ""); is called elsewhere.
// Otherwise iframes would get stuck in this interaction mode
iframe.contentWindow.close = function(){
iframe.contentWindow.close = function () {
iframe.$window && iframe.$window.close();
};
// @TODO: hook into saveAs (a la FileSaver.js) and another function for opening files
@ -402,12 +402,12 @@ function $Iframe(options){
// saveAsDialog();
// };
} catch(error) {
} catch (error) {
console.log("Failed to reach into iframe; got", error);
}
});
if (options.src) {
$iframe.attr({src: options.src});
$iframe.attr({ src: options.src });
}
$iframe.css({
minWidth: 0,
@ -469,14 +469,14 @@ function $Iframe(options){
// Fix dragging things (i.e. windows) over iframes (i.e. other windows)
// (when combined with a bit of css, .dragging iframe { pointer-events: none; })
// (and a similar thing in $IframeWindow)
$(window).on("pointerdown", function(e){
$(window).on("pointerdown", function (e) {
//console.log(e.type);
$("body").addClass("dragging");
});
$(window).on("pointerup dragend blur", function(e){
$(window).on("pointerup dragend blur", function (e) {
//console.log(e.type);
if(e.type === "blur"){
if(document.activeElement.tagName.match(/iframe/i)){
if (e.type === "blur") {
if (document.activeElement.tagName.match(/iframe/i)) {
return;
}
}

View File

@ -6,30 +6,30 @@ const TAU = //////|//////
/// -' 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 $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}`;
}
function E(t){
function E(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. */
function debounce(func, wait_ms, immediate) {
let timeout;
const debounced_func = function() {
const debounced_func = function () {
const context = this;
const args = arguments;
const later = ()=> {
const later = () => {
timeout = null;
if (!immediate) {
func.apply(context, args);
@ -66,17 +66,17 @@ function debounce(func, wait_ms, immediate) {
return debounced_func;
}
function memoize_synchronous_function(func, max_entries=50000) {
function memoize_synchronous_function(func, max_entries = 50000) {
const cache = {};
const keys = [];
const memoized_func = (...args)=> {
if (args.some((arg)=> arg instanceof CanvasPattern)) {
const memoized_func = (...args) => {
if (args.some((arg) => arg instanceof CanvasPattern)) {
return func.apply(null, args);
}
const key = JSON.stringify(args);
if (cache[key]){
if (cache[key]) {
return cache[key];
} else{
} else {
const val = func.apply(null, args);
cache[key] = val;
keys.push(key);
@ -87,7 +87,7 @@ function memoize_synchronous_function(func, max_entries=50000) {
return val;
}
}
memoized_func.clear_memo_cache = ()=> {
memoized_func.clear_memo_cache = () => {
for (const key of keys) {
delete cache[key];
}
@ -96,7 +96,7 @@ function memoize_synchronous_function(func, max_entries=50000) {
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);
single_pixel_canvas.ctx.fillStyle = color;
@ -130,7 +130,7 @@ function image_data_match(a, b, threshold) {
return true;
}
function make_canvas(width, height){
function make_canvas(width, height) {
const image = width;
const new_canvas = E("canvas");
@ -138,7 +138,7 @@ function make_canvas(width, height){
new_canvas.ctx = new_ctx;
new_ctx.disable_image_smoothing = ()=> {
new_ctx.disable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = false;
// condition is to avoid a deprecation warning in Firefox
if (new_ctx.imageSmoothingEnabled !== false) {
@ -147,7 +147,7 @@ function make_canvas(width, height){
new_ctx.msImageSmoothingEnabled = false;
}
};
new_ctx.enable_image_smoothing = ()=> {
new_ctx.enable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = true;
if (new_ctx.imageSmoothingEnabled !== true) {
new_ctx.mozImageSmoothingEnabled = true;
@ -174,13 +174,13 @@ function make_canvas(width, height){
}
};
if(width && height){
if (width && height) {
// make_canvas(width, height)
new_canvas.width = width;
new_canvas.height = height;
// setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing();
}else if(image){
} else if (image) {
// make_canvas(image)
new_ctx.copy(image);
}
@ -200,11 +200,11 @@ function get_icon_for_tool(tool) {
// not to be confused with load_image_from_uri
function load_image_simple(src) {
return new Promise((resolve, reject)=> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = ()=> { resolve(img); };
img.onerror = ()=> { reject(new Error(`failed to load image from ${src}`)); };
img.onload = () => { resolve(img); };
img.onerror = () => { reject(new Error(`failed to load image from ${src}`)); };
img.src = src;
});
@ -216,9 +216,9 @@ function get_icon_for_tools(tools) {
}
const icon_canvas = make_canvas(16, 16);
Promise.all(tools.map((tool)=> load_image_simple(`help/${tool.help_icon}`)))
.then((icons)=> {
icons.forEach((icon, i)=> {
Promise.all(tools.map((tool) => load_image_simple(`help/${tool.help_icon}`)))
.then((icons) => {
icons.forEach((icon, i) => {
const w = icon_canvas.width / icons.length;
const x = i * w;
const h = icon_canvas.height;

View File

@ -1,76 +1,76 @@
const fill_threshold = 1; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184
function get_brush_canvas_size(brush_size, brush_shape){
function get_brush_canvas_size(brush_size, brush_shape) {
// brush_shape optional, only matters if it's circle
// @TODO: does it actually still matter? the ellipse drawing code has changed
// round to nearest even number in order for the canvas to be drawn centered at a point reasonably
return Math.ceil(brush_size * (brush_shape === "circle" ? 2.1 : 1) / 2) * 2;
}
function render_brush(ctx, shape, size){
function render_brush(ctx, shape, size) {
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of draw_ellipse)
if(shape.match(/diagonal/)){
if (shape.match(/diagonal/)) {
size -= 0.4;
}
const mid_x = Math.round(ctx.canvas.width / 2);
const left = Math.round(mid_x - size/2);
const right = Math.round(mid_x + size/2);
const left = Math.round(mid_x - size / 2);
const right = Math.round(mid_x + size / 2);
const mid_y = Math.round(ctx.canvas.height / 2);
const top = Math.round(mid_y - size/2);
const bottom = Math.round(mid_y + size/2);
const top = Math.round(mid_y - size / 2);
const bottom = Math.round(mid_y + size / 2);
if(shape === "circle"){
if (shape === "circle") {
// @TODO: ideally _without_pattern_support
draw_ellipse(ctx, left, top, size, size, false, true);
// was useful for testing:
// ctx.fillStyle = "red";
// ctx.fillRect(mid_x, mid_y, 1, 1);
}else if(shape === "square"){
} else if (shape === "square") {
ctx.fillRect(left, top, ~~size, ~~size);
}else if(shape === "diagonal"){
} else if (shape === "diagonal") {
draw_line_without_pattern_support(ctx, left, top, right, bottom);
}else if(shape === "reverse_diagonal"){
} else if (shape === "reverse_diagonal") {
draw_line_without_pattern_support(ctx, left, bottom, right, top);
}else if(shape === "horizontal"){
} else if (shape === "horizontal") {
draw_line_without_pattern_support(ctx, left, mid_y, size, mid_y);
}else if(shape === "vertical"){
} else if (shape === "vertical") {
draw_line_without_pattern_support(ctx, mid_x, top, mid_x, size);
}
}
function draw_ellipse(ctx, x, y, w, h, stroke, fill){
const center_x = x + w/2;
const center_y = y + h/2;
function draw_ellipse(ctx, x, y, w, h, stroke, fill) {
const center_x = x + w / 2;
const center_y = y + h / 2;
if(aliasing){
if (aliasing) {
const points = [];
const step = 0.05;
for(let theta = 0; theta < TAU; theta += step){
for (let theta = 0; theta < TAU; theta += step) {
points.push({
x: center_x + Math.cos(theta) * w/2,
y: center_y + Math.sin(theta) * h/2,
x: center_x + Math.cos(theta) * w / 2,
y: center_y + Math.sin(theta) * h / 2,
});
}
draw_polygon(ctx, points, stroke, fill);
}else{
} else {
ctx.beginPath();
ctx.ellipse(center_x, center_y, Math.abs(w/2), Math.abs(h/2), 0, TAU, false);
ctx.ellipse(center_x, center_y, Math.abs(w / 2), Math.abs(h / 2), 0, TAU, false);
ctx.stroke();
ctx.fill();
}
}
function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, stroke, fill){
function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, stroke, fill) {
if(aliasing){
if (aliasing) {
const points = [];
const lineTo = (x, y)=> {
points.push({x, y});
const lineTo = (x, y) => {
points.push({ x, y });
};
const arc = (x, y, radius_x, radius_y, startAngle, endAngle)=> {
const arc = (x, y, radius_x, radius_y, startAngle, endAngle) => {
const step = 0.05;
for(let theta = startAngle; theta < endAngle; theta += step){
for (let theta = startAngle; theta < endAngle; theta += step) {
points.push({
x: x + Math.cos(theta) * radius_x,
y: y + Math.sin(theta) * radius_y,
@ -85,16 +85,16 @@ function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, st
const x2 = x + width;
const y2 = y + height;
arc(x2 - radius_x, y + radius_y, radius_x, radius_y, TAU*3/4, TAU, false);
arc(x2 - radius_x, y + radius_y, radius_x, radius_y, TAU * 3 / 4, TAU, false);
lineTo(x2, y2 - radius_y);
arc(x2 - radius_x, y2 - radius_y, radius_x, radius_y, 0, TAU*1/4, false);
arc(x2 - radius_x, y2 - radius_y, radius_x, radius_y, 0, TAU * 1 / 4, false);
lineTo(x + radius_x, y2);
arc(x + radius_x, y2 - radius_y, radius_x, radius_y, TAU*1/4, TAU*1/2, false);
arc(x + radius_x, y2 - radius_y, radius_x, radius_y, TAU * 1 / 4, TAU * 1 / 2, false);
lineTo(x, y + radius_y);
arc(x + radius_x, y + radius_y, radius_x, radius_y, TAU/2, TAU*3/4, false);
arc(x + radius_x, y + radius_y, radius_x, radius_y, TAU / 2, TAU * 3 / 4, false);
draw_polygon(ctx, points, stroke, fill);
}else{
} else {
ctx.beginPath();
ctx.moveTo(x + radius_x, y);
ctx.lineTo(x + width - radius_x, y);
@ -106,10 +106,10 @@ function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, st
ctx.lineTo(x, y + radius_y);
ctx.quadraticCurveTo(x, y, x + radius_x, y);
ctx.closePath();
if(stroke){
if (stroke) {
ctx.stroke();
}
if(fill){
if (fill) {
ctx.fill();
}
}
@ -117,7 +117,7 @@ function draw_rounded_rectangle(ctx, x, y, width, height, radius_x, radius_y, st
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
// @TODO: protect against browser clearing canvases, invalidate cache
const get_brush_canvas = memoize_synchronous_function((brush_shape, brush_size)=> {
const get_brush_canvas = memoize_synchronous_function((brush_shape, brush_size) => {
const canvas_size = get_brush_canvas_size(brush_size, brush_shape);
const brush_canvas = make_canvas(canvas_size, canvas_size);
@ -128,13 +128,13 @@ const get_brush_canvas = memoize_synchronous_function((brush_shape, brush_size)=
return brush_canvas;
}, 20); // 12 brush tool options + current brush + current pencil + current eraser + current shape stroke + a few
$G.on("invalidate-brush-canvases", ()=> {
$G.on("invalidate-brush-canvases", () => {
get_brush_canvas.clear_memo_cache();
});
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
const stamp_brush_canvas = (ctx, x, y, brush_shape, brush_size)=> {
const stamp_brush_canvas = (ctx, x, y, brush_shape, brush_size) => {
const brush_canvas = get_brush_canvas(brush_shape, brush_size);
const offset_x = -Math.ceil(brush_canvas.width / 2);
@ -144,7 +144,7 @@ const stamp_brush_canvas = (ctx, x, y, brush_shape, brush_size)=> {
};
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
const get_circumference_points_for_brush = memoize_synchronous_function((brush_shape, brush_size)=> {
const get_circumference_points_for_brush = memoize_synchronous_function((brush_shape, brush_size) => {
const brush_canvas = get_brush_canvas(brush_shape, brush_size);
@ -181,31 +181,31 @@ const get_circumference_points_for_brush = memoize_synchronous_function((brush_s
return points;
});
$G.on("invalidate-brush-canvases", ()=> {
$G.on("invalidate-brush-canvases", () => {
get_circumference_points_for_brush.clear_memo_cache();
});
let line_brush_canvas;
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
function update_brush_for_drawing_lines(stroke_size){
if(aliasing && stroke_size > 1){
function update_brush_for_drawing_lines(stroke_size) {
if (aliasing && stroke_size > 1) {
line_brush_canvas = get_brush_canvas("circle", stroke_size);
}
}
function draw_line_without_pattern_support(ctx, x1, y1, x2, y2, stroke_size = 1) {
if(aliasing){
if(stroke_size > 1){
if (aliasing) {
if (stroke_size > 1) {
bresenham_line(x1, y1, x2, y2, (x, y) => {
ctx.drawImage(line_brush_canvas, ~~(x - line_brush_canvas.width/2), ~~(y - line_brush_canvas.height/2));
ctx.drawImage(line_brush_canvas, ~~(x - line_brush_canvas.width / 2), ~~(y - line_brush_canvas.height / 2));
});
}else{
} else {
bresenham_line(x1, y1, x2, y2, (x, y) => {
ctx.fillRect(x, y, 1, 1);
});
}
}else{
} else {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
@ -217,9 +217,9 @@ function draw_line_without_pattern_support(ctx, x1, y1, x2, y2, stroke_size = 1)
}
}
function bresenham_line(x1, y1, x2, y2, callback){
function bresenham_line(x1, y1, x2, y2, callback) {
// Bresenham's line algorithm
x1=~~x1; x2=~~x2; y1=~~y1; y2=~~y2;
x1 = ~~x1; x2 = ~~x2; y1 = ~~y1; y2 = ~~y2;
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
@ -228,19 +228,19 @@ function bresenham_line(x1, y1, x2, y2, callback){
let err = dx - dy;
// eslint-disable-next-line no-constant-condition
while(true){
while (true) {
callback(x1, y1);
if(x1===x2 && y1===y2) break;
const e2 = err*2;
if(e2 >-dy){ err -= dy; x1 += sx; }
if(e2 < dx){ err += dx; y1 += sy; }
if (x1 === x2 && y1 === y2) break;
const e2 = err * 2;
if (e2 > -dy) { err -= dy; x1 += sx; }
if (e2 < dx) { err += dx; y1 += sy; }
}
}
function bresenham_dense_line(x1, y1, x2, y2, callback){
function bresenham_dense_line(x1, y1, x2, y2, callback) {
// Bresenham's line algorithm with a callback between going horizontal and vertical
x1=~~x1; x2=~~x2; y1=~~y1; y2=~~y2;
x1 = ~~x1; x2 = ~~x2; y1 = ~~y1; y2 = ~~y2;
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
@ -249,18 +249,18 @@ function bresenham_dense_line(x1, y1, x2, y2, callback){
let err = dx - dy;
// eslint-disable-next-line no-constant-condition
while(true){
while (true) {
callback(x1, y1);
if(x1===x2 && y1===y2) break;
const e2 = err*2;
if(e2 >-dy){ err -= dy; x1 += sx; }
if (x1 === x2 && y1 === y2) break;
const e2 = err * 2;
if (e2 > -dy) { err -= dy; x1 += sx; }
callback(x1, y1);
if(e2 < dx){ err += dx; y1 += sy; }
if (e2 < dx) { err += dx; y1 += sy; }
}
}
function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g, fill_b, fill_a){
function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g, fill_b, fill_a) {
// @TODO: split up processing in case it takes too long?
// progress bar and abort button (outside of image-manipulation.js)
@ -277,11 +277,11 @@ function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g
start_y = Math.max(0, Math.min(Math.floor(start_y), c_height));
const stack = [[start_x, start_y]];
const id = ctx.getImageData(0, 0, c_width, c_height);
let pixel_pos = (start_y*c_width + start_x) * 4;
const start_r = id.data[pixel_pos+0];
const start_g = id.data[pixel_pos+1];
const start_b = id.data[pixel_pos+2];
const start_a = id.data[pixel_pos+3];
let pixel_pos = (start_y * c_width + start_x) * 4;
const start_r = id.data[pixel_pos + 0];
const start_g = id.data[pixel_pos + 1];
const start_b = id.data[pixel_pos + 2];
const start_a = id.data[pixel_pos + 3];
// @TODO: Allow flood-filling colors similar within fill threshold.
// Right now it will cause an infinite loop if we don't stop early in this case.
@ -296,7 +296,7 @@ function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g
return;
}
while(stack.length){
while (stack.length) {
let new_pos;
let x;
let y;
@ -306,42 +306,42 @@ function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g
x = new_pos[0];
y = new_pos[1];
pixel_pos = (y*c_width + x) * 4;
while(should_fill_at(pixel_pos)){
pixel_pos = (y * c_width + x) * 4;
while (should_fill_at(pixel_pos)) {
y--;
pixel_pos = (y*c_width + x) * 4;
pixel_pos = (y * c_width + x) * 4;
}
reach_left = false;
reach_right = false;
// eslint-disable-next-line no-constant-condition
while(true){
while (true) {
y++;
pixel_pos = (y*c_width + x) * 4;
pixel_pos = (y * c_width + x) * 4;
if(!(y < c_height && should_fill_at(pixel_pos))){
if (!(y < c_height && should_fill_at(pixel_pos))) {
break;
}
do_fill_at(pixel_pos);
if(x > 0){
if(should_fill_at(pixel_pos - 4)){
if(!reach_left){
if (x > 0) {
if (should_fill_at(pixel_pos - 4)) {
if (!reach_left) {
stack.push([x - 1, y]);
reach_left = true;
}
}else if(reach_left){
} else if (reach_left) {
reach_left = false;
}
}
if(x < c_width-1){
if(should_fill_at(pixel_pos + 4)){
if(!reach_right){
if (x < c_width - 1) {
if (should_fill_at(pixel_pos + 4)) {
if (!reach_right) {
stack.push([x + 1, y]);
reach_right = true;
}
}else if(reach_right){
} else if (reach_right) {
reach_right = false;
}
}
@ -351,21 +351,21 @@ function draw_fill_without_pattern_support(ctx, start_x, start_y, fill_r, fill_g
}
ctx.putImageData(id, 0, 0);
function should_fill_at(pixel_pos){
function should_fill_at(pixel_pos) {
return (
// matches start color (i.e. region to fill)
Math.abs(id.data[pixel_pos+0] - start_r) <= fill_threshold &&
Math.abs(id.data[pixel_pos+1] - start_g) <= fill_threshold &&
Math.abs(id.data[pixel_pos+2] - start_b) <= fill_threshold &&
Math.abs(id.data[pixel_pos+3] - start_a) <= fill_threshold
Math.abs(id.data[pixel_pos + 0] - start_r) <= fill_threshold &&
Math.abs(id.data[pixel_pos + 1] - start_g) <= fill_threshold &&
Math.abs(id.data[pixel_pos + 2] - start_b) <= fill_threshold &&
Math.abs(id.data[pixel_pos + 3] - start_a) <= fill_threshold
);
}
function do_fill_at(pixel_pos){
id.data[pixel_pos+0] = fill_r;
id.data[pixel_pos+1] = fill_g;
id.data[pixel_pos+2] = fill_b;
id.data[pixel_pos+3] = fill_a;
function do_fill_at(pixel_pos) {
id.data[pixel_pos + 0] = fill_r;
id.data[pixel_pos + 1] = fill_g;
id.data[pixel_pos + 2] = fill_b;
id.data[pixel_pos + 3] = fill_a;
}
}
@ -393,13 +393,13 @@ function draw_fill_separately(source_ctx, dest_ctx, start_x, start_y, fill_r, fi
const stack = [[start_x, start_y]];
const source_id = source_ctx.getImageData(0, 0, c_width, c_height);
const dest_id = dest_ctx.getImageData(0, 0, c_width, c_height);
let pixel_pos = (start_y*c_width + start_x) * 4;
const start_r = source_id.data[pixel_pos+0];
const start_g = source_id.data[pixel_pos+1];
const start_b = source_id.data[pixel_pos+2];
const start_a = source_id.data[pixel_pos+3];
let pixel_pos = (start_y * c_width + start_x) * 4;
const start_r = source_id.data[pixel_pos + 0];
const start_g = source_id.data[pixel_pos + 1];
const start_b = source_id.data[pixel_pos + 2];
const start_a = source_id.data[pixel_pos + 3];
while(stack.length){
while (stack.length) {
let new_pos;
let x;
let y;
@ -409,42 +409,42 @@ function draw_fill_separately(source_ctx, dest_ctx, start_x, start_y, fill_r, fi
x = new_pos[0];
y = new_pos[1];
pixel_pos = (y*c_width + x) * 4;
while(should_fill_at(pixel_pos)){
pixel_pos = (y * c_width + x) * 4;
while (should_fill_at(pixel_pos)) {
y--;
pixel_pos = (y*c_width + x) * 4;
pixel_pos = (y * c_width + x) * 4;
}
reach_left = false;
reach_right = false;
// eslint-disable-next-line no-constant-condition
while(true){
while (true) {
y++;
pixel_pos = (y*c_width + x) * 4;
pixel_pos = (y * c_width + x) * 4;
if(!(y < c_height && should_fill_at(pixel_pos))){
if (!(y < c_height && should_fill_at(pixel_pos))) {
break;
}
do_fill_at(pixel_pos);
if(x > 0){
if(should_fill_at(pixel_pos - 4)){
if(!reach_left){
if (x > 0) {
if (should_fill_at(pixel_pos - 4)) {
if (!reach_left) {
stack.push([x - 1, y]);
reach_left = true;
}
}else if(reach_left){
} else if (reach_left) {
reach_left = false;
}
}
if(x < c_width-1){
if(should_fill_at(pixel_pos + 4)){
if(!reach_right){
if (x < c_width - 1) {
if (should_fill_at(pixel_pos + 4)) {
if (!reach_right) {
stack.push([x + 1, y]);
reach_right = true;
}
}else if(reach_right){
} else if (reach_right) {
reach_right = false;
}
}
@ -454,49 +454,49 @@ function draw_fill_separately(source_ctx, dest_ctx, start_x, start_y, fill_r, fi
}
dest_ctx.putImageData(dest_id, 0, 0);
function should_fill_at(pixel_pos){
function should_fill_at(pixel_pos) {
return (
// not reached yet
dest_id.data[pixel_pos+3] === 0 &&
dest_id.data[pixel_pos + 3] === 0 &&
// and matches start color (i.e. region to fill)
(
Math.abs(source_id.data[pixel_pos+0] - start_r) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos+1] - start_g) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos+2] - start_b) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos+3] - start_a) <= fill_threshold
Math.abs(source_id.data[pixel_pos + 0] - start_r) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos + 1] - start_g) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos + 2] - start_b) <= fill_threshold &&
Math.abs(source_id.data[pixel_pos + 3] - start_a) <= fill_threshold
)
);
}
function do_fill_at(pixel_pos){
dest_id.data[pixel_pos+0] = fill_r;
dest_id.data[pixel_pos+1] = fill_g;
dest_id.data[pixel_pos+2] = fill_b;
dest_id.data[pixel_pos+3] = fill_a;
function do_fill_at(pixel_pos) {
dest_id.data[pixel_pos + 0] = fill_r;
dest_id.data[pixel_pos + 1] = fill_g;
dest_id.data[pixel_pos + 2] = fill_b;
dest_id.data[pixel_pos + 3] = fill_a;
}
}
function replace_color_globally(image_data, from_r, from_g, from_b, from_a, to_r, to_g, to_b, to_a) {
if(
if (
from_r === to_r &&
from_g === to_g &&
from_b === to_b &&
from_a === to_a
){
) {
return;
}
const {data} = image_data;
for(let i = 0; i < data.length; i += 4){
if(
Math.abs(data[i+0] - from_r) <= fill_threshold &&
Math.abs(data[i+1] - from_g) <= fill_threshold &&
Math.abs(data[i+2] - from_b) <= fill_threshold &&
Math.abs(data[i+3] - from_a) <= fill_threshold
){
data[i+0] = to_r;
data[i+1] = to_g;
data[i+2] = to_b;
data[i+3] = to_a;
const { data } = image_data;
for (let i = 0; i < data.length; i += 4) {
if (
Math.abs(data[i + 0] - from_r) <= fill_threshold &&
Math.abs(data[i + 1] - from_g) <= fill_threshold &&
Math.abs(data[i + 2] - from_b) <= fill_threshold &&
Math.abs(data[i + 3] - from_a) <= fill_threshold
) {
data[i + 0] = to_r;
data[i + 1] = to_g;
data[i + 2] = to_b;
data[i + 3] = to_a;
}
}
}
@ -504,17 +504,17 @@ function replace_color_globally(image_data, from_r, from_g, from_b, from_a, to_r
function find_color_globally(source_image_data, dest_image_data, find_r, find_g, find_b, find_a) {
const source_data = source_image_data.data;
const dest_data = dest_image_data.data;
for(let i = 0; i < source_data.length; i += 4){
if(
Math.abs(source_data[i+0] - find_r) <= fill_threshold &&
Math.abs(source_data[i+1] - find_g) <= fill_threshold &&
Math.abs(source_data[i+2] - find_b) <= fill_threshold &&
Math.abs(source_data[i+3] - find_a) <= fill_threshold
){
dest_data[i+0] = 255;
dest_data[i+1] = 255;
dest_data[i+2] = 255;
dest_data[i+3] = 255;
for (let i = 0; i < source_data.length; i += 4) {
if (
Math.abs(source_data[i + 0] - find_r) <= fill_threshold &&
Math.abs(source_data[i + 1] - find_g) <= fill_threshold &&
Math.abs(source_data[i + 2] - find_b) <= fill_threshold &&
Math.abs(source_data[i + 3] - find_a) <= fill_threshold
) {
dest_data[i + 0] = 255;
dest_data[i + 1] = 255;
dest_data[i + 2] = 255;
dest_data[i + 3] = 255;
}
}
}
@ -523,18 +523,18 @@ function draw_noncontiguous_fill_without_pattern_support(ctx, x, y, fill_r, fill
x = Math.max(0, Math.min(Math.floor(x), ctx.canvas.width));
y = Math.max(0, Math.min(Math.floor(y), ctx.canvas.height));
const image_data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const start_index = (y*image_data.width + x) * 4;
const start_r = image_data.data[start_index+0];
const start_g = image_data.data[start_index+1];
const start_b = image_data.data[start_index+2];
const start_a = image_data.data[start_index+3];
const start_index = (y * image_data.width + x) * 4;
const start_r = image_data.data[start_index + 0];
const start_g = image_data.data[start_index + 1];
const start_b = image_data.data[start_index + 2];
const start_a = image_data.data[start_index + 3];
replace_color_globally(image_data, start_r, start_g, start_b, start_a, fill_r, fill_g, fill_b, fill_a);
ctx.putImageData(image_data, 0, 0);
}
function draw_noncontiguous_fill(ctx, x, y, swatch){
function draw_noncontiguous_fill(ctx, x, y, swatch) {
if (typeof swatch === "string") {
const fill_rgba = get_rgba_from_color(swatch);
draw_noncontiguous_fill_without_pattern_support(ctx, x, y, fill_rgba[0], fill_rgba[1], fill_rgba[2], fill_rgba[3]);
@ -552,20 +552,20 @@ function draw_noncontiguous_fill_separately(source_ctx, dest_ctx, x, y) {
y = Math.max(0, Math.min(Math.floor(y), source_ctx.canvas.height));
const source_image_data = source_ctx.getImageData(0, 0, source_ctx.canvas.width, source_ctx.canvas.height);
const dest_image_data = dest_ctx.getImageData(0, 0, dest_ctx.canvas.width, dest_ctx.canvas.height);
const start_index = (y*source_image_data.width + x) * 4;
const start_r = source_image_data.data[start_index+0];
const start_g = source_image_data.data[start_index+1];
const start_b = source_image_data.data[start_index+2];
const start_a = source_image_data.data[start_index+3];
const start_index = (y * source_image_data.width + x) * 4;
const start_r = source_image_data.data[start_index + 0];
const start_g = source_image_data.data[start_index + 1];
const start_b = source_image_data.data[start_index + 2];
const start_a = source_image_data.data[start_index + 3];
find_color_globally(source_image_data, dest_image_data, start_r, start_g, start_b, start_a);
dest_ctx.putImageData(dest_image_data, 0, 0);
}
function apply_image_transformation(meta, fn){
function apply_image_transformation(meta, fn) {
// Apply an image transformation function to either the selection or the entire canvas
const original_canvas = selection ? selection.source_canvas: main_canvas;
const original_canvas = selection ? selection.source_canvas : main_canvas;
const new_canvas = make_canvas(original_canvas.width, original_canvas.height);
@ -574,7 +574,7 @@ function apply_image_transformation(meta, fn){
fn(original_canvas, original_ctx, new_canvas, new_ctx);
if(selection){
if (selection) {
undoable({
name: `${meta.name} (${localize("Selection")})`,
icon: meta.icon,
@ -582,7 +582,7 @@ function apply_image_transformation(meta, fn){
}, () => {
selection.replace_source_canvas(new_canvas);
});
}else{
} else {
deselect();
cancel();
undoable({
@ -599,7 +599,7 @@ function apply_image_transformation(meta, fn){
}
}
function flip_horizontal(){
function flip_horizontal() {
apply_image_transformation({
name: localize("Flip horizontal"),
icon: get_help_folder_icon("p_fliph.png"),
@ -610,7 +610,7 @@ function flip_horizontal(){
});
}
function flip_vertical(){
function flip_vertical() {
apply_image_transformation({
name: localize("Flip vertical"),
icon: get_help_folder_icon("p_flipv.png"),
@ -621,15 +621,15 @@ function flip_vertical(){
});
}
function rotate(angle){
function rotate(angle) {
apply_image_transformation({
name: `${localize("Rotate by angle")} ${angle / TAU * 360} ${localize("Degrees")}`,
icon: get_help_folder_icon(`p_rotate_${angle >= 0 ? "cw" : "ccw"}.png`),
}, (original_canvas, original_ctx, new_canvas, new_ctx) => {
new_ctx.save();
switch(angle){
switch (angle) {
case TAU / 4:
case TAU * -3/4:
case TAU * -3 / 4:
new_canvas.width = original_canvas.height;
new_canvas.height = original_canvas.width;
new_ctx.disable_image_smoothing();
@ -641,7 +641,7 @@ function rotate(angle){
new_ctx.translate(new_canvas.width, new_canvas.height);
new_ctx.rotate(TAU / 2);
break;
case TAU * 3/4:
case TAU * 3 / 4:
case TAU / -4:
new_canvas.width = original_canvas.height;
new_canvas.height = original_canvas.width;
@ -658,8 +658,8 @@ function rotate(angle){
let bb_min_y = +Infinity;
let bb_max_y = -Infinity;
const corner = (x01, y01) => {
const x = Math.sin(-angle)*h*x01 + Math.cos(+angle)*w*y01;
const y = Math.sin(+angle)*w*y01 + Math.cos(-angle)*h*x01;
const x = Math.sin(-angle) * h * x01 + Math.cos(+angle) * w * y01;
const y = Math.sin(+angle) * w * y01 + Math.cos(-angle) * h * x01;
bb_min_x = Math.min(bb_min_x, x);
bb_max_x = Math.max(bb_max_x, x);
bb_min_y = Math.min(bb_min_y, y);
@ -680,12 +680,12 @@ function rotate(angle){
new_canvas.height = bb_h;
new_ctx.disable_image_smoothing();
if(!transparency){
if (!transparency) {
new_ctx.fillStyle = selected_colors.background;
new_ctx.fillRect(0, 0, new_canvas.width, new_canvas.height);
}
new_ctx.translate(-bb_x,-bb_y);
new_ctx.translate(-bb_x, -bb_y);
new_ctx.rotate(angle);
new_ctx.drawImage(original_canvas, 0, 0, w, h);
break;
@ -696,7 +696,7 @@ function rotate(angle){
});
}
function stretch_and_skew(x_scale, y_scale, h_skew, v_skew){
function stretch_and_skew(x_scale, y_scale, h_skew, v_skew) {
apply_image_transformation({
name:
(h_skew !== 0 || v_skew !== 0) ? (
@ -718,8 +718,8 @@ function stretch_and_skew(x_scale, y_scale, h_skew, v_skew){
let bb_min_y = +Infinity;
let bb_max_y = -Infinity;
const corner = (x01, y01) => {
const x = Math.tan(h_skew)*h*x01 + w*y01;
const y = Math.tan(v_skew)*w*y01 + h*x01;
const x = Math.tan(h_skew) * h * x01 + w * y01;
const y = Math.tan(v_skew) * w * y01 + h * x01;
bb_min_x = Math.min(bb_min_x, x);
bb_max_x = Math.max(bb_max_x, x);
bb_min_y = Math.min(bb_min_y, y);
@ -740,7 +740,7 @@ function stretch_and_skew(x_scale, y_scale, h_skew, v_skew){
new_canvas.height = Math.max(1, bb_h);
new_ctx.disable_image_smoothing();
if(!transparency){
if (!transparency) {
new_ctx.fillStyle = selected_colors.background;
new_ctx.fillRect(0, 0, new_canvas.width, new_canvas.height);
}
@ -759,17 +759,17 @@ function stretch_and_skew(x_scale, y_scale, h_skew, v_skew){
});
}
function invert_rgb(source_ctx, dest_ctx=source_ctx) {
function invert_rgb(source_ctx, dest_ctx = source_ctx) {
const image_data = source_ctx.getImageData(0, 0, source_ctx.canvas.width, source_ctx.canvas.height);
for(let i=0; i<image_data.data.length; i+=4){
image_data.data[i+0] = 255 - image_data.data[i+0];
image_data.data[i+1] = 255 - image_data.data[i+1];
image_data.data[i+2] = 255 - image_data.data[i+2];
for (let i = 0; i < image_data.data.length; i += 4) {
image_data.data[i + 0] = 255 - image_data.data[i + 0];
image_data.data[i + 1] = 255 - image_data.data[i + 1];
image_data.data[i + 2] = 255 - image_data.data[i + 2];
}
dest_ctx.putImageData(image_data, 0, 0);
}
function invert_monochrome(source_ctx, dest_ctx=source_ctx, monochrome_info=detect_monochrome(source_ctx)) {
function invert_monochrome(source_ctx, dest_ctx = source_ctx, monochrome_info = detect_monochrome(source_ctx)) {
const image_data = source_ctx.getImageData(0, 0, source_ctx.canvas.width, source_ctx.canvas.height);
// Note: values in pixel_array may be different on big endian vs little endian machines.
// Only rely on equality of values within the array.
@ -808,7 +808,7 @@ function invert_monochrome(source_ctx, dest_ctx=source_ctx, monochrome_info=dete
return;
}
const [uint32_a, uint32_b] = monochrome_info.presentNonTransparentUint32s;
for(let i=0, len=pixel_array.length; i<len; i+=1){
for (let i = 0, len = pixel_array.length; i < len; i += 1) {
if (pixel_array[i] === uint32_a) {
pixel_array[i] = uint32_b;
} else if (pixel_array[i] === uint32_b) {
@ -820,17 +820,17 @@ function invert_monochrome(source_ctx, dest_ctx=source_ctx, monochrome_info=dete
function threshold_black_and_white(ctx, threshold) {
const image_data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
for(let i=0; i<image_data.data.length; i+=4){
const white = (image_data.data[i+0] + image_data.data[i+1] + image_data.data[i+2]) / 3 / 255 > threshold;
image_data.data[i+0] = 255 * white;
image_data.data[i+1] = 255 * white;
image_data.data[i+2] = 255 * white;
image_data.data[i+3] = 255;
for (let i = 0; i < image_data.data.length; i += 4) {
const white = (image_data.data[i + 0] + image_data.data[i + 1] + image_data.data[i + 2]) / 3 / 255 > threshold;
image_data.data[i + 0] = 255 * white;
image_data.data[i + 1] = 255 * white;
image_data.data[i + 2] = 255 * white;
image_data.data[i + 3] = 255;
}
ctx.putImageData(image_data, 0, 0);
}
function replace_colors_with_swatch(ctx, swatch, x_offset_from_global_canvas=0, y_offset_from_global_canvas=0){
function replace_colors_with_swatch(ctx, swatch, x_offset_from_global_canvas = 0, y_offset_from_global_canvas = 0) {
// USAGE NOTE: Context MUST be untranslated! (for the rectangle to cover the exact area of the canvas, and presumably for the pattern alignment as well)
// This function is mainly for patterns support (for black & white mode) but naturally handles solid colors as well.
ctx.globalCompositeOperation = "source-in";
@ -844,27 +844,27 @@ function replace_colors_with_swatch(ctx, swatch, x_offset_from_global_canvas=0,
}
// adapted from https://github.com/Pomax/bezierjs
function compute_bezier(t, start_x, start_y, control_1_x, control_1_y, control_2_x, control_2_y, end_x, end_y){
const mt = 1-t;
const mt2 = mt*mt;
const t2 = t*t;
function compute_bezier(t, start_x, start_y, control_1_x, control_1_y, control_2_x, control_2_y, end_x, end_y) {
const mt = 1 - t;
const mt2 = mt * mt;
const t2 = t * t;
let a, b, c, d = 0;
a = mt2*mt;
b = mt2*t*3;
c = mt*t2*3;
d = t*t2;
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
return {
x: a*start_x + b*control_1_x + c*control_2_x + d*end_x,
y: a*start_y + b*control_1_y + c*control_2_y + d*end_y
x: a * start_x + b * control_1_x + c * control_2_x + d * end_x,
y: a * start_y + b * control_1_y + c * control_2_y + d * end_y
};
}
function draw_bezier_curve_without_pattern_support(ctx, start_x, start_y, control_1_x, control_1_y, control_2_x, control_2_y, end_x, end_y, stroke_size) {
const steps = 100;
let point_a = {x: start_x, y: start_y};
for(let t=0; t<1; t+=1/steps){
let point_a = { x: start_x, y: start_y };
for (let t = 0; t < 1; t += 1 / steps) {
const point_b = compute_bezier(t, start_x, start_y, control_1_x, control_1_y, control_2_x, control_2_y, end_x, end_y);
// @TODO: carry "error" from Bresenham line algorithm between iterations? and/or get a proper Bezier drawing algorithm
draw_line_without_pattern_support(ctx, point_a.x, point_a.y, point_b.x, point_b.y, stroke_size);
@ -886,7 +886,7 @@ function draw_bezier_curve(ctx, start_x, start_y, control_1_x, control_1_y, cont
draw_bezier_curve_without_pattern_support(op_ctx_2d, start_x, start_y, control_1_x, control_1_y, control_2_x, control_2_y, end_x, end_y, stroke_size);
});
}
function draw_line(ctx, x1, y1, x2, y2, stroke_size){
function draw_line(ctx, x1, y1, x2, y2, stroke_size) {
const min_x = Math.min(x1, x2);
const min_y = Math.min(y1, y2);
const max_x = Math.max(x1, x2);
@ -910,7 +910,7 @@ function draw_grid(ctx, scale) {
grid_pattern_canvas.ctx.fillStyle = dark_gray;
grid_pattern_canvas.ctx.fillRect(0, 0, pattern_size, 1);
grid_pattern_canvas.ctx.fillStyle = light_gray;
for (let i=1; i<pattern_size; i+=2) {
for (let i = 1; i < pattern_size; i += 2) {
grid_pattern_canvas.ctx.fillRect(i, 0, 1, 1);
grid_pattern_canvas.ctx.fillRect(0, i, 1, 1);
}
@ -954,7 +954,7 @@ function draw_grid(ctx, scale) {
}
const dash_width = 1;
const hairline_width = 1/scale; // size of a screen pixel
const hairline_width = 1 / scale; // size of a screen pixel
ctx.save();
@ -968,14 +968,14 @@ function draw_grid(ctx, scale) {
if (go_x > 0) {
const matrix = svg_for_creating_matrices.createSVGMatrix();
if (horizontal_pattern.setTransform) { // not supported by Edge as of 2019-12-04
horizontal_pattern.setTransform(matrix.translate(-x, -y).translate(hairline_width, 0).scale(1/scale));
horizontal_pattern.setTransform(matrix.translate(-x, -y).translate(hairline_width, 0).scale(1 / scale));
}
ctx.fillStyle = horizontal_pattern;
ctx.fillRect(0, 0, go_x, dash_width);
} else if(go_y > 0) {
} else if (go_y > 0) {
const matrix = svg_for_creating_matrices.createSVGMatrix();
if (vertical_pattern.setTransform) { // not supported by Edge as of 2019-12-04
vertical_pattern.setTransform(matrix.translate(-x, -y).translate(0, hairline_width).scale(1/scale));
vertical_pattern.setTransform(matrix.translate(-x, -y).translate(0, hairline_width).scale(1 / scale));
}
ctx.fillStyle = vertical_pattern;
ctx.fillRect(0, 0, dash_width, go_y);
@ -983,16 +983,16 @@ function draw_grid(ctx, scale) {
ctx.restore();
}
window.draw_selection_box = (ctx, rect_x, rect_y, rect_w, rect_h, scale, translate_x, translate_y)=> {
draw_dashes(ctx, rect_x , rect_y , rect_w - 1, 0 , scale, translate_x, translate_y); // top
window.draw_selection_box = (ctx, rect_x, rect_y, rect_w, rect_h, scale, translate_x, translate_y) => {
draw_dashes(ctx, rect_x, rect_y, rect_w - 1, 0, scale, translate_x, translate_y); // top
if (rect_h === 1) {
draw_dashes(ctx, rect_x , rect_y , 0 , 1 , scale, translate_x, translate_y); // left
draw_dashes(ctx, rect_x, rect_y, 0, 1, scale, translate_x, translate_y); // left
} else {
draw_dashes(ctx, rect_x , rect_y + 1 , 0 , rect_h - 2, scale, translate_x, translate_y); // left
draw_dashes(ctx, rect_x, rect_y + 1, 0, rect_h - 2, scale, translate_x, translate_y); // left
}
draw_dashes(ctx, rect_x + rect_w - 1, rect_y , 0 , rect_h , scale, translate_x, translate_y); // right
draw_dashes(ctx, rect_x , rect_y + rect_h -1, rect_w - 1, 0 , scale, translate_x, translate_y); // bottom
draw_dashes(ctx, rect_x , rect_y + 1 , 0 , 1 , scale, translate_x, translate_y); // top left dangling bit???
draw_dashes(ctx, rect_x + rect_w - 1, rect_y, 0, rect_h, scale, translate_x, translate_y); // right
draw_dashes(ctx, rect_x, rect_y + rect_h - 1, rect_w - 1, 0, scale, translate_x, translate_y); // bottom
draw_dashes(ctx, rect_x, rect_y + 1, 0, 1, scale, translate_x, translate_y); // top left dangling bit???
};
})();
@ -1150,16 +1150,16 @@ function draw_grid(ctx, scale) {
initWebGL(op_canvas_webgl);
let warning_tid;
op_canvas_webgl.addEventListener("webglcontextlost", (e)=> {
op_canvas_webgl.addEventListener("webglcontextlost", (e) => {
e.preventDefault();
window.console && console.warn("WebGL context lost");
clamp_brush_sizes();
warning_tid = setTimeout(()=> {
warning_tid = setTimeout(() => {
show_error_message("The WebGL context was lost. You may need to refresh the web page, or restart your computer.");
}, 3000);
}, false);
op_canvas_webgl.addEventListener("webglcontextrestored", ()=> {
op_canvas_webgl.addEventListener("webglcontextrestored", () => {
initWebGL(op_canvas_webgl);
window.console && console.warn("WebGL context restored");
@ -1197,7 +1197,7 @@ function draw_grid(ctx, scale) {
draw_polygon_or_line_strip(ctx, points, stroke, fill, true);
};
function draw_polygon_or_line_strip(ctx, points, stroke, fill, close_path){
function draw_polygon_or_line_strip(ctx, points, stroke, fill, close_path) {
if (!gl) {
show_error_message("Failed to get WebGL context. You may need to refresh the web page, or restart your computer.");
return; // @TODO: don't pollute brush cache with empty brushes (also maybe fallback to 2D canvas rendering)
@ -1207,7 +1207,7 @@ function draw_grid(ctx, scale) {
// otherwise update_brush_for_drawing_lines calls render_brush calls draw_ellipse calls draw_polygon calls draw_polygon_or_line_strip
// trying to use the same op_canvas
// (also, avoiding infinite recursion by checking for stroke; assuming brushes will never have outlines)
if(stroke && stroke_size > 1){
if (stroke && stroke_size > 1) {
update_brush_for_drawing_lines(stroke_size);
}
@ -1217,7 +1217,7 @@ function draw_grid(ctx, scale) {
const numPoints = points.length;
const numCoords = numPoints * 2;
if(numPoints === 0){
if (numPoints === 0) {
return;
}
@ -1225,7 +1225,7 @@ function draw_grid(ctx, scale) {
let x_max = -Infinity;
let y_min = +Infinity;
let y_max = -Infinity;
for (const {x, y} of points) {
for (const { x, y } of points) {
x_min = Math.min(x, x_min);
x_max = Math.max(x, x_max);
y_min = Math.min(y, y_min);
@ -1242,12 +1242,12 @@ function draw_grid(ctx, scale) {
const coords = new Float32Array(numCoords);
for (let i = 0; i < numPoints; i++) {
coords[i*2+0] = (points[i].x - x_min) / op_canvas_webgl.width * 2 - 1;
coords[i*2+1] = 1 - (points[i].y - y_min) / op_canvas_webgl.height * 2;
coords[i * 2 + 0] = (points[i].x - x_min) / op_canvas_webgl.width * 2 - 1;
coords[i * 2 + 1] = 1 - (points[i].y - y_min) / op_canvas_webgl.height * 2;
// @TODO: investigate: does this cause resolution/information loss? can we change the coordinate system?
}
if(fill){
if (fill) {
const contours = [coords];
const polyTriangles = triangulate(contours);
let numVertices = initArrayBuffer(polyTriangles);
@ -1261,8 +1261,8 @@ function draw_grid(ctx, scale) {
replace_colors_with_swatch(op_ctx_2d, fill_color, x_min, y_min);
ctx.drawImage(op_canvas_2d, x_min, y_min);
}
if(stroke){
if(stroke_size > 1){
if (stroke) {
if (stroke_size > 1) {
const stroke_margin = ~~(stroke_size * 1.1);
const op_canvas_x = x_min - stroke_margin;
@ -1286,7 +1286,7 @@ function draw_grid(ctx, scale) {
replace_colors_with_swatch(op_ctx_2d, stroke_color, op_canvas_x, op_canvas_y);
ctx.drawImage(op_canvas_2d, op_canvas_x, op_canvas_y);
}else{
} else {
let numVertices = initArrayBuffer(coords);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(close_path ? gl.LINE_LOOP : gl.LINE_STRIP, 0, numVertices);

View File

@ -1,7 +1,7 @@
let $imgur_window;
function show_imgur_uploader(blob){
if($imgur_window){
function show_imgur_uploader(blob) {
if ($imgur_window) {
$imgur_window.close();
}
$imgur_window = $DialogWindow().title("Upload To Imgur").addClass("horizontal-buttons");
@ -17,7 +17,7 @@ function show_imgur_uploader(blob){
display: "block", // prevent margin below due to inline display (vertical-align can also be used)
});
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_area.css({
maxWidth: "90vw",
@ -26,7 +26,7 @@ function show_imgur_uploader(blob){
marginBottom: "0.5em",
});
$preview_image.on("load", () => {
$imgur_window.css({width: "auto"});
$imgur_window.css({ width: "auto" });
$imgur_window.center();
});
$imgur_window.on("close", () => {
@ -53,7 +53,7 @@ function show_imgur_uploader(blob){
const parseImgurResponseJSON = responseJSON => {
try {
return JSON.parse(responseJSON);
} catch(error) {
} catch (error) {
$imgur_status.text("Received an invalid JSON response from Imgur: ");
// .append($(E("pre")).text(responseJSON));
@ -82,7 +82,7 @@ function show_imgur_uploader(blob){
width: "500px",
overflow: "auto",
});
$imgur_window.css({width: "auto"});
$imgur_window.css({ width: "auto" });
$imgur_window.center();
}
};
@ -91,9 +91,9 @@ function show_imgur_uploader(blob){
const req = new XMLHttpRequest();
if(req.upload){
if (req.upload) {
req.upload.addEventListener('progress', event => {
if(event.lengthComputable){
if (event.lengthComputable) {
const progress_value = event.loaded / event.total;
const percentage_text = `${Math.floor(progress_value * 100)}%`;
$progress.val(progress_value);
@ -103,13 +103,13 @@ function show_imgur_uploader(blob){
}
req.addEventListener("readystatechange", () => {
if(req.readyState == 4 && req.status == 200){
if (req.readyState == 4 && req.status == 200) {
$progress.add($progress_percent).remove();
const response = parseImgurResponseJSON(req.responseText);
if(!response) return;
if (!response) return;
if(!response.success){
if (!response.success) {
$imgur_status.text("Failed to upload image :(");
return;
}
@ -117,7 +117,7 @@ function show_imgur_uploader(blob){
$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.attr('href', url);
@ -132,21 +132,21 @@ function show_imgur_uploader(blob){
const req = new XMLHttpRequest();
$delete_button[0].disabled = true;
req.addEventListener("readystatechange", () => {
if(req.readyState == 4 && req.status == 200){
if (req.readyState == 4 && req.status == 200) {
$delete_button.remove();
$ok_button.focus();
const response = parseImgurResponseJSON(req.responseText);
if(!response) return;
if (!response) return;
if(response.success){
if (response.success) {
$imgur_url_area.remove();
$imgur_status.text("Deleted successfully");
}else{
} else {
$imgur_status.text("Failed to delete image :(");
}
}else if(req.readyState == 4){
} else if (req.readyState == 4) {
$imgur_status.text("Error deleting image :(");
$delete_button[0].disabled = false;
$delete_button.focus();
@ -164,7 +164,7 @@ function show_imgur_uploader(blob){
$ok_button = $imgur_window.$Button(localize("OK"), () => {
$imgur_window.close();
}).focus();
}else if(req.readyState == 4){
} else if (req.readyState == 4) {
$progress.add($progress_percent).remove();
$imgur_status.text("Error uploading image :(");
}

View File

@ -3,12 +3,12 @@ let $storage_manager;
let $quota_exceeded_window;
let ignoring_quota_exceeded = false;
async function storage_quota_exceeded(){
if($quota_exceeded_window){
async function storage_quota_exceeded() {
if ($quota_exceeded_window) {
$quota_exceeded_window.close();
$quota_exceeded_window = null;
}
if(ignoring_quota_exceeded){
if (ignoring_quota_exceeded) {
return;
}
const { promise, $window } = showMessageBox({
@ -34,8 +34,8 @@ async function storage_quota_exceeded(){
}
}
function manage_storage(){
if($storage_manager){
function manage_storage() {
if ($storage_manager) {
$storage_manager.close();
}
$storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager squish");
@ -54,8 +54,8 @@ function manage_storage(){
const $img = $(E("img")).attr({ src: imgSrc }).addClass("thumbnail-img");
const $remove = $(E("button")).text("Remove").addClass("remove-button");
const href = `#${k.replace("image#", "local:")}`;
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 $open_link = $(E("a")).attr({ href, target: "_blank" }).text(localize("Open"));
const $thumbnail_open_link = $(E("a")).attr({ href, target: "_blank" }).addClass("thumbnail-container");
$thumbnail_open_link.append($img);
$(E("td")).append($thumbnail_open_link).appendTo($tr);
$(E("td")).append($open_link).appendTo($tr);
@ -64,7 +64,7 @@ function manage_storage(){
$remove.on("click", () => {
localStorage.removeItem(k);
$tr.remove();
if($table.find("tr").length == 0){
if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}
});
@ -83,18 +83,18 @@ function manage_storage(){
delete localStorage._available;
}
// eslint-disable-next-line no-empty
} catch (e) {}
} catch (e) { }
if (localStorageAvailable) {
for(const k in localStorage){
if(k.match(/^image#/)){
for (const k in localStorage) {
if (k.match(/^image#/)) {
let v = localStorage[k];
try {
if (v[0] === '"') {
v = JSON.parse(v);
}
// eslint-disable-next-line no-empty
} catch (e) {}
} catch (e) { }
addRow(k, v);
}
}
@ -104,7 +104,7 @@ function manage_storage(){
// @TODO: DRY with similar message
// @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>");
} else if($table.find("tr").length == 0) {
} else if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}

View File

@ -1,4 +1,4 @@
(()=> {
(() => {
const looksLikeChrome = !!(window.chrome && (window.chrome.loadTimes || window.chrome.csi));
// NOTE: Microsoft Edge includes window.chrome.app
@ -12,7 +12,7 @@ window.menus = {
speech_recognition: [
"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."),
},
{
@ -24,7 +24,7 @@ window.menus = {
"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",
],
action: ()=> { file_open(); },
action: () => { file_open(); },
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 the document", "download the file", "download the image", "download the image file",
],
action: ()=> { file_save(); },
action: () => { file_save(); },
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 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."),
},
MENU_DIVIDER,
@ -98,7 +98,7 @@ window.menus = {
"load picture from address",
"load picture from web address",
],
action: ()=> { file_load_from_url(); },
action: () => { file_load_from_url(); },
description: localize("Opens an image from the web."),
},
{
@ -106,12 +106,12 @@ window.menus = {
speech_recognition: [
"upload to imgur", "upload image to imgur", "upload picture to imgur",
],
action: ()=> {
action: () => {
// include the selection in the saved image
deselect();
main_canvas.toBlob((blob)=> {
sanity_check_blob(blob, ()=> {
main_canvas.toBlob((blob) => {
sanity_check_blob(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",
"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."),
},
MENU_DIVIDER,
@ -135,7 +135,7 @@ window.menus = {
speech_recognition: [
"preview print", "print preview", "show print preview", "show preview of print",
],
action: ()=> {
action: () => {
print();
},
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",
"page setup", "printing setup", "page set-up", "printing set-up", "page set up", "printing set up",
],
action: ()=> {
action: () => {
print();
},
description: localize("Prints the active document and sets printing options."),
@ -167,7 +167,7 @@ window.menus = {
"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",
],
action: ()=> {
action: () => {
print();
},
description: localize("Prints the active document and sets printing options."),
@ -183,7 +183,7 @@ window.menus = {
"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",
],
action: ()=> { systemHooks.setWallpaperTiled(main_canvas); },
action: () => { systemHooks.setWallpaperTiled(main_canvas); },
description: localize("Tiles this bitmap as the desktop background."),
},
{
@ -195,7 +195,7 @@ window.menus = {
"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",
],
action: ()=> { systemHooks.setWallpaperCentered(main_canvas); },
action: () => { systemHooks.setWallpaperCentered(main_canvas); },
description: localize("Centers this bitmap as the desktop background."),
},
MENU_DIVIDER,
@ -240,7 +240,7 @@ window.menus = {
"undo", "undo that",
],
enabled: () => undos.length >= 1,
action: ()=> { undo(); },
action: () => { undo(); },
description: localize("Undoes the last action."),
},
{
@ -250,7 +250,7 @@ window.menus = {
"repeat", "redo",
],
enabled: () => redos.length >= 1,
action: ()=> { redo(); },
action: () => { redo(); },
description: localize("Redoes the previously undone action."),
},
{
@ -259,7 +259,7 @@ window.menus = {
speech_recognition: [
"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."),
},
MENU_DIVIDER,
@ -272,7 +272,7 @@ window.menus = {
enabled: () =>
// @TODO: support cutting text with this menu item as well (e.g. for the text tool)
!!selection,
action: ()=> {
action: () => {
edit_cut(true);
},
description: localize("Cuts the selection and puts it on the Clipboard."),
@ -286,7 +286,7 @@ window.menus = {
enabled: () =>
// @TODO: support copying text with this menu item as well (e.g. for the text tool)
!!selection,
action: ()=> {
action: () => {
edit_copy(true);
},
description: localize("Copies the selection and puts it on the Clipboard."),
@ -300,7 +300,7 @@ window.menus = {
enabled: () =>
// @TODO: disable if nothing in clipboard or wrong type (if we can access that)
true,
action: ()=> {
action: () => {
edit_paste(true);
},
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",
],
enabled: () => !!selection,
action: ()=> { delete_selection(); },
action: () => { delete_selection(); },
description: localize("Deletes the selection."),
},
{
@ -323,7 +323,7 @@ window.menus = {
"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",
],
action: ()=> { select_all(); },
action: () => { select_all(); },
description: localize("Selects everything."),
},
MENU_DIVIDER,
@ -337,7 +337,7 @@ window.menus = {
"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,
action: ()=> { save_selection_to_file(); },
action: () => { save_selection_to_file(); },
description: localize("Copies the selection to a file."),
},
{
@ -345,7 +345,7 @@ window.menus = {
speech_recognition: [
"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."),
}
],
@ -358,7 +358,7 @@ window.menus = {
// @TODO: hide/show
],
checkbox: {
toggle: ()=> {
toggle: () => {
$toolbox.toggle();
},
check: () => $toolbox.is(":visible"),
@ -373,7 +373,7 @@ window.menus = {
// @TODO: hide/show
],
checkbox: {
toggle: ()=> {
toggle: () => {
$colorbox.toggle();
},
check: () => $colorbox.is(":visible"),
@ -387,7 +387,7 @@ window.menus = {
// @TODO: hide/show
],
checkbox: {
toggle: ()=> {
toggle: () => {
$status_area.toggle();
},
check: () => $status_area.is(":visible"),
@ -422,7 +422,7 @@ window.menus = {
],
description: localize("Zooms the picture to 100%."),
enabled: () => magnification !== 1,
action: ()=> {
action: () => {
set_magnification(1);
},
},
@ -438,7 +438,7 @@ window.menus = {
],
description: localize("Zooms the picture to 400%."),
enabled: () => magnification !== 4,
action: ()=> {
action: () => {
set_magnification(4);
},
},
@ -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",
],
description: localize("Zooms the picture to fit within the view."),
action: ()=> {
action: () => {
const rect = $canvas_area[0].getBoundingClientRect();
const margin = 30; // leave a margin so scrollbars won't appear
let mag = Math.min(
@ -482,7 +482,7 @@ window.menus = {
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",
],
action: ()=> { show_custom_zoom_window(); },
action: () => { show_custom_zoom_window(); },
},
MENU_DIVIDER,
{
@ -527,7 +527,7 @@ window.menus = {
"show image fullscreen", "show image full-screen", "show image full screen",
// @TODO: exit fullscreen
],
action: ()=> { view_bitmap(); },
action: () => { view_bitmap(); },
description: localize("Displays the entire picture."),
},
MENU_DIVIDER,
@ -567,7 +567,7 @@ window.menus = {
"flip/rotate", "flip slash rotate", "flip and rotate", "flip or rotate", "flip rotate",
// @TODO: parameters to command
],
action: ()=> { image_flip_and_rotate(); },
action: () => { image_flip_and_rotate(); },
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",
// @TODO: parameters to command
],
action: ()=> { image_stretch_and_skew(); },
action: () => { image_stretch_and_skew(); },
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 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."),
},
{
@ -606,7 +606,7 @@ window.menus = {
"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",
],
action: ()=> { image_attributes(); },
action: () => { image_attributes(); },
description: localize("Changes the attributes of the picture."),
},
{
@ -617,8 +617,8 @@ window.menus = {
// @TODO: erase?
],
// (mspaint says "Ctrl+Shft+N")
action: ()=> { !selection && clear(); },
enabled: ()=> !selection,
action: () => { !selection && clear(); },
enabled: () => !selection,
description: localize("Clears the picture."),
// action: ()=> {
// if (selection) {
@ -641,7 +641,7 @@ window.menus = {
// @TODO: hide/show / "draw opaque" / "draw transparent"/translucent?
],
checkbox: {
toggle: ()=> {
toggle: () => {
tool_transparent_mode = !tool_transparent_mode;
$G.trigger("option-changed");
},
@ -658,7 +658,7 @@ window.menus = {
"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",
],
action: ()=> {
action: () => {
show_edit_colors_window();
},
description: localize("Creates a new color."),
@ -668,15 +668,15 @@ window.menus = {
speech_recognition: [
"get colors", "load colors", "load color palette", "load palette", "load color palette file", "load palette file", "load list of colors",
],
action: async ()=> {
const {file} = await systemHooks.showOpenFileDialog({formats: palette_formats});
AnyPalette.loadPalette(file, (error, new_palette)=> {
action: async () => {
const { file } = await systemHooks.showOpenFileDialog({ formats: palette_formats });
AnyPalette.loadPalette(file, (error, new_palette) => {
if (error) {
show_file_format_errors({ as_palette_error: error });
} else {
palette = new_palette.map((color)=> color.toString());
palette = new_palette.map((color) => color.toString());
$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: [
"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();
ap.name = "JS Paint Saved Colors";
ap.numberOfColumns = 16; // 14?
@ -703,11 +703,11 @@ window.menus = {
dialogTitle: localize("Save Colors"),
defaultFileName: localize("untitled.pal"),
formats: palette_formats,
getBlob: ()=> {
getBlob: () => {
const file_content = AnyPalette.writePalette(ap, AnyPalette.formats[format_id]);
const blob = new Blob([file_content], {type: "text/plain"});
return new Promise((resolve)=> {
sanity_check_blob(blob, ()=> {
const blob = new Blob([file_content], { type: "text/plain" });
return new Promise((resolve) => {
sanity_check_blob(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 viewer", "show help viewer", "open help viewer",
],
action: ()=> { show_help(); },
action: () => { show_help(); },
description: localize("Displays Help for the current task or command."),
},
MENU_DIVIDER,
@ -736,7 +736,7 @@ window.menus = {
"application info", "about the application", "application information", "information about the application",
"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 program information, version number, and copyright."),
}
@ -748,7 +748,7 @@ window.menus = {
speech_recognition: [
// 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."),
},
{
@ -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",
"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."),
},
// {
@ -854,7 +854,7 @@ window.menus = {
"collaborative",
"collaborating",
],
action: ()=> {
action: () => {
show_multi_user_setup_dialog(true);
},
description: localize("Starts a new multi-user session from the current document."),
@ -904,7 +904,7 @@ window.menus = {
"start collaboration with empty",
"start collaborating with empty",
],
action: ()=> {
action: () => {
show_multi_user_setup_dialog(false);
},
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",
"go light", "go bright",
],
action: ()=> {
action: () => {
set_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",
"go dark", "go dim",
],
action: ()=> {
action: () => {
set_theme("dark.css");
},
enabled: () => get_theme() != "dark.css",
@ -965,7 +965,7 @@ window.menus = {
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",
],
action: ()=> {
action: () => {
set_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",
"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");
},
enabled: () => get_theme() != "winter.css",
@ -1023,12 +1023,12 @@ window.menus = {
"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",
"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",
"begin ritual", "begin the ritual", "begin a ritual",
"start ritual", "start the ritual", "start a ritual",
],
action: ()=> {
action: () => {
set_theme("occult.css");
},
enabled: () => get_theme() != "occult.css",
@ -1038,10 +1038,10 @@ window.menus = {
},
{
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),
action: ()=> {
action: () => {
set_language(available_language);
},
enabled: () => get_language() != available_language,
@ -1092,7 +1092,7 @@ window.menus = {
"stop eye gazing",
],
checkbox: {
toggle: ()=> {
toggle: () => {
if (location.hash.match(/eye-gaze-mode/i)) {
// @TODO: confirmation dialog that you could cancel with dwell clicking!
// if (confirm("This will disable eye gaze mode.")) {
@ -1102,7 +1102,7 @@ window.menus = {
change_url_param("eye-gaze-mode", true);
}
},
check: ()=> {
check: () => {
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",
],
checkbox: {
toggle: ()=> {
toggle: () => {
if (location.hash.match(/speech-recognition-mode/i)) {
change_url_param("speech-recognition-mode", false);
} else {
change_url_param("speech-recognition-mode", true);
}
},
check: ()=> {
check: () => {
return window.speech_recognition_active;
},
},
enabled: ()=> window.speech_recognition_available,
enabled: () => window.speech_recognition_available,
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.
],
checkbox: {
toggle: ()=> {
toggle: () => {
if (location.hash.match(/eye-gaze-mode/i)) {
// @TODO: confirmation dialog that you could cancel with dwell clicking!
// if (confirm("This will disable eye gaze mode.")) {
@ -1156,11 +1156,11 @@ window.menus = {
change_url_param("vertical-color-box-mode", true);
}
},
check: ()=> {
check: () => {
return location.hash.match(/vertical-color-box-mode|eye-gaze-mode/i);
},
},
enabled: ()=> {
enabled: () => {
return !location.hash.match(/eye-gaze-mode/i);
},
description: localize("Arranges the color box vertically."),
@ -1171,7 +1171,7 @@ window.menus = {
speech_recognition: [
// 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."),
},
MENU_DIVIDER,
@ -1184,7 +1184,7 @@ window.menus = {
"what's new", "new features",
"show news", "show news update", "news update",
],
action: ()=> { show_news(); },
action: () => { show_news(); },
description: localize("Shows news about JS Paint."),
},
{
@ -1192,7 +1192,7 @@ window.menus = {
speech_recognition: [
"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."),
},
{
@ -1200,7 +1200,7 @@ window.menus = {
speech_recognition: [
"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."),
},
],
@ -1208,7 +1208,7 @@ window.menus = {
for (const [top_level_menu_key, menu] of Object.entries(menus)) {
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) {
if (menu_item !== MENU_DIVIDER) {
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_matchers = menu_item_matchers.map((menu_item_matcher)=> {
menu_item_matchers = menu_item_matchers.map((menu_item_matcher) => {
return `${ancestor_names} ${menu_item_matcher}`;
});
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);
};
@ -11,7 +11,7 @@
localStorageAvailable = localStorage._available;
delete localStorage._available;
// eslint-disable-next-line no-empty
} catch (e) {}
} catch (e) { }
// @TODO: keep other data in addition to the image data
// such as the file_name and other state
@ -19,25 +19,24 @@
// 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 canvas_has_any_apparent_image_data = ()=>
main_canvas.ctx.getImageData(0, 0, main_canvas.width, main_canvas.height).data.some((v)=> v > match_threshold);
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);
let $recovery_window;
function show_recovery_window(no_longer_blank) {
$recovery_window && $recovery_window.close();
const $w = $recovery_window = $DialogWindow();
$w.on("close", ()=> {
$w.on("close", () => {
$recovery_window = null;
});
$w.title("Recover Document");
let backup_impossible = false;
try{window.localStorage}catch(e){backup_impossible = true;}
try { window.localStorage } catch (e) { backup_impossible = true; }
$w.$main.append($(`
<h1>Woah!</h1>
<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>
${
backup_impossible ?
${backup_impossible ?
"<p><b>Note:</b> No automatic backup is possible unless you enable Cookies in your browser.</p>"
: (
no_longer_blank ?
@ -55,20 +54,20 @@
}
`));
const $undo = $w.$Button("Undo", ()=> {
const $undo = $w.$Button("Undo", () => {
undo();
});
const $redo = $w.$Button("Redo", ()=> {
const $redo = $w.$Button("Redo", () => {
redo();
});
const update_buttons_disabled = ()=> {
const update_buttons_disabled = () => {
$undo.attr("disabled", undos.length < 1);
$redo.attr("disabled", redos.length < 1);
};
$G.on("session-update.session-hook", update_buttons_disabled);
update_buttons_disabled();
$w.$Button(localize("Close"), ()=> {
$w.$Button(localize("Close"), () => {
$w.close();
});
$w.center();
@ -188,7 +187,7 @@
// Unused
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
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
@ -359,7 +358,7 @@
};
this.write_canvas_to_database_soon = debounce(this.write_canvas_to_database_immediately, 100);
let ignore_session_update = false;
$G.on("session-update.session-hook", ()=> {
$G.on("session-update.session-hook", () => {
if (ignore_session_update) {
log("(Ignore session-update from Sync Session undoable)");
return;
@ -394,7 +393,7 @@
undoable({
name: "Sync Session",
icon: get_help_folder_icon("p_database.png"),
}, ()=> {
}, () => {
// Write the image data to the canvas
main_ctx.copy(img);
$canvas_area.trigger("resize");
@ -432,7 +431,7 @@
away: false,
});
});
$G.on("blur.session-hook", ()=> {
$G.on("blur.session-hook", () => {
this.fb_user.child("cursor").update({
away: true,
});
@ -498,45 +497,45 @@
let current_session;
const end_current_session = () => {
if(current_session){
if (current_session) {
log("Ending current session");
current_session.end();
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 session_match = location.hash.match(/^#?(?:.*,)?(session|local):(.*)$/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 session_id = session_match[2];
if(session_id === ""){
if (session_id === "") {
log("Invalid session ID; session ID cannot be empty");
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 ./[]#$");
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");
end_current_session();
}else if(
} else if (
current_session && current_session.id === session_id &&
local === (current_session instanceof LocalSession)
){
) {
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
end_current_session();
if(local){
if (local) {
log(`Starting a new LocalSession, ID: ${session_id}`);
current_session = new LocalSession(session_id);
}else{
} else {
log(`Starting a new MultiUserSession, ID: ${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 uris = get_uris(url);
@ -554,11 +553,11 @@
open_from_image_info(info, null, null, true, true);
}, show_resource_load_error_message);
}else{
} else {
log("No session ID in hash");
const old_hash = location.hash;
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);
if (old_hash === location.hash) {
// e.g. on Wayback Machine

View File

@ -1,4 +1,4 @@
(()=>{
(() => {
let seed = 4; // chosen later
@ -15,8 +15,8 @@ window.simulatingGestures = false;
let gestureTimeoutID;
let periodicGesturesTimeoutID;
let choose = (array)=> array[~~(seededRandom() * array.length)];
let isAnyMenuOpen = ()=> $(".menu-button.active").length > 0;
let choose = (array) => array[~~(seededRandom() * array.length)];
let isAnyMenuOpen = () => $(".menu-button.active").length > 0;
let cursor_image = new Image();
cursor_image.src = "images/cursors/default.png";
@ -32,7 +32,7 @@ $cursor.css({
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 canvasAreaRect = $canvas_area[0].getBoundingClientRect();
@ -168,7 +168,7 @@ window.simulateRandomGesturesPeriodically = () => {
simulateRandomGesturesPeriodically();
delete window.drawRandomlySeed;
`);
`);
let delayBetweenGestures = 500;
let shiftStart = false;
@ -188,7 +188,7 @@ window.simulateRandomGesturesPeriodically = () => {
$canvas_area.scrollTop($canvas_area.width() * seededRandom());
$canvas_area.scrollLeft($canvas_area.height() * seededRandom());
let _simulateRandomGesture = (callback)=> {
let _simulateRandomGesture = (callback) => {
window.simulateRandomGesture(callback, {
shift: shiftStart,
shiftToggleChance,
@ -212,7 +212,7 @@ window.simulateRandomGesturesPeriodically = () => {
}
if (seededRandom() < switchToolsChance) {
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) {
$(choose($(".tool-options *"))).trigger("click");
@ -222,9 +222,9 @@ window.simulateRandomGesturesPeriodically = () => {
let secondary = seededRandom() < 0.5;
const colorButton = choose($(".swatch, .color-button"));
$(colorButton)
.trigger($.Event("pointerdown", {button: secondary ? 2 : 0}))
.trigger($.Event("click", {button: secondary ? 2 : 0}))
.trigger($.Event("pointerup", {button: secondary ? 2 : 0}));
.trigger($.Event("pointerdown", { button: secondary ? 2 : 0 }))
.trigger($.Event("click", { button: secondary ? 2 : 0 }))
.trigger($.Event("pointerup", { button: secondary ? 2 : 0 }));
}
if (seededRandom() < scrollChance) {
let scrollAmount = (seededRandom() * 2 - 1) * 700;
@ -235,7 +235,7 @@ window.simulateRandomGesturesPeriodically = () => {
}
}
periodicGesturesTimeoutID = setTimeout(() => {
_simulateRandomGesture(()=> {
_simulateRandomGesture(() => {
if (selection && seededRandom() < dragSelectionChance) {
window.simulateRandomGesture(waitThenGo, {
shift: shiftStart,

View File

@ -1,4 +1,4 @@
(function() {
(function () {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
@ -1251,16 +1251,16 @@ const recognitionFixes = [
// 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 toolNames = tools.map((tool)=> tool.speech_recognition).flat();
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();
// @TODO: select foreground/background/ternary color specifically
// @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: "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: select tool options like selection opacity and brush sizes
// 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"
// 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"
// @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?
@ -1290,13 +1290,13 @@ recognition.maxAlternatives = 1;
window.speech_recognition_active = false;
window.enable_speech_recognition = function() {
window.enable_speech_recognition = function () {
if (!window.speech_recognition_active) {
window.speech_recognition_active = true;
recognition.start();
}
};
window.disable_speech_recognition = function() {
window.disable_speech_recognition = function () {
if (window.speech_recognition_active) {
window.speech_recognition_active = false;
recognition.stop();
@ -1321,7 +1321,7 @@ function fix_up_speech_recognition(command) {
}
return command;
}
recognition.onresult = function(event) {
recognition.onresult = function (event) {
if (document.visibilityState !== "visible") {
return;
}
@ -1343,10 +1343,9 @@ recognition.onresult = function(event) {
const interpretations = interpret_command(command, true);
if (interpretations.length) {
const interpretation = choose_interpretation(interpretations);
$status_text.html(`Speech:&nbsp;<span style="white-space: pre;">${
command.replace(
$status_text.html(`Speech:&nbsp;<span style="white-space: pre;">${command.replace(
new RegExp(escapeRegExp(interpretation.match_text), "i"),
(important_text)=> `<b>${important_text}</b>`,
(important_text) => `<b>${important_text}</b>`,
)
}</span>`);
console.log(`Interpreting command "${command}" as`, interpretation);
@ -1357,35 +1356,35 @@ recognition.onresult = function(event) {
}
};
recognition.onspeechend = function() {
recognition.addEventListener("end", ()=> {
recognition.onspeechend = function () {
recognition.addEventListener("end", () => {
recognition.start();
}, {once: true});
}, { once: true });
recognition.stop();
};
recognition.onnomatch = function(event) {
recognition.onnomatch = function (event) {
if (document.visibilityState !== "visible") {
return;
}
$status_text.text("Speech not recognized.");
};
recognition.onstart = function(event) {
recognition.onstart = function (event) {
window.speech_recognition_active = true;
};
recognition.onend = function(event) {
recognition.onend = function (event) {
window.speech_recognition_active = false;
};
recognition.onerror = function(event) {
recognition.onerror = function (event) {
if (event.error.toString().match(/no-speech/)) {
try {
recognition.start();
} catch(error) {
recognition.addEventListener("end", ()=> {
} catch (error) {
recognition.addEventListener("end", () => {
recognition.start();
}, {once: true});
}, { once: true });
}
} else {
$status_text.text('Error occurred in speech recognition: ' + event.error);
@ -1411,9 +1410,9 @@ function choose_interpretation(interpretations) {
return best_interpretation;
}
window.interpret_command = (input_text, default_to_entering_text)=> {
window.interpret_command = (input_text, default_to_entering_text) => {
const interpretations = [];
const add_interpretation = (interpretation)=> {
const add_interpretation = (interpretation) => {
interpretations.push(interpretation);
};
@ -1421,7 +1420,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (` ${input_text} `.toLowerCase().indexOf(` ${color.toLowerCase()} `) !== -1) {
add_interpretation({
match_text: color,
exec: ((color)=> ()=> {
exec: ((color) => () => {
selected_colors.foreground = color;
$G.trigger("option-changed");
})(color),
@ -1436,7 +1435,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({
match_text: select_tool_match[0],
tool_id: tool.id,
exec: ((tool)=> ()=> {
exec: ((tool) => () => {
select_tool(tool);
})(tool),
});
@ -1445,7 +1444,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
}
const all_menu_items = [];
const collect_menu_items = (menu)=> {
const collect_menu_items = (menu) => {
for (const menu_item of menu) {
if (menu_item !== MENU_DIVIDER) {
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) {
add_interpretation({
match_text: menu_item_phrase,
exec: ((menu_item)=> ()=> {
exec: ((menu_item) => () => {
if (menu_item.checkbox) {
menu_item.checkbox.toggle();
} else {
@ -1480,7 +1479,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (close_menus_match) {
add_interpretation({
match_text: close_menus_match[0],
exec: ()=> {
exec: () => {
// from close_menus in $MenuBar
$(".menu-button").trigger("release");
// Close any rogue floating submenus
@ -1499,7 +1498,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({
match_text: draw_match[0],
sketch_subject: subject_matter,
exec: ()=> {
exec: () => {
find_clipart_and_sketch(subject_matter);
},
});
@ -1648,7 +1647,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (` ${input_text} `.toLowerCase().indexOf(` ${match_phrase.toLowerCase()} `) !== -1) {
add_interpretation({
match_text: match_phrase,
exec: ((button)=> ()=> {
exec: ((button) => () => {
clickButtonVisibly(button);
console.log("click", button);
})(button),
@ -1666,7 +1665,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
add_interpretation({
match_text: stop_match[0],
type: "stop-drawing",
exec: ()=> {
exec: () => {
window.stopSimulatingGestures && window.stopSimulatingGestures();
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({
match_text: line_width_match[0],
size: n,
exec: ()=> {
exec: () => {
if (isFinite(n)) {
// @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));
}else if(selected_tool.id === TOOL_ERASER){
} else if (selected_tool.id === TOOL_ERASER) {
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));
}else if(selected_tool.id === TOOL_PENCIL){
} else if (selected_tool.id === TOOL_PENCIL) {
pencil_size = Math.max(1, Math.min(n, 50));
}else if(
} else if (
selected_tool.id === TOOL_LINE ||
selected_tool.id === TOOL_CURVE ||
selected_tool.id === TOOL_RECTANGLE ||
@ -1733,8 +1732,8 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
}
$G.trigger("option-changed");
if(button !== undefined && pointer){ // pointer may only be needed for tests
selected_tools.forEach((selected_tool)=> {
if (button !== undefined && pointer) { // pointer may only be needed for tests
selected_tools.forEach((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);
if (scroll_match) {
const directions = scroll_match[0];
const vector = {x: 0, y: 0};
const vector = { x: 0, y: 0 };
if (directions.match(/up|north/i)) {
vector.y = -1;
}
@ -1764,11 +1763,11 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (directions.match(/right|east/i)) {
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({
match_text: scroll_match[0],
exec: ()=> {
const factor = directions.match(/page/) ? 1 : 1/2;
exec: () => {
const factor = directions.match(/page/) ? 1 : 1 / 2;
// 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).animate({
@ -1786,7 +1785,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
if (new_line_match) {
add_interpretation({
match_text: new_line_match[0],
exec: ()=> {
exec: () => {
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");
add_interpretation({
match_text: input_text,
exec: ()=> {
exec: () => {
if (document.activeElement && document.activeElement.matches("input[type='number']")) {
document.activeElement.value = input_text;
} else {
@ -1836,7 +1835,7 @@ window.interpret_command = (input_text, default_to_entering_text)=> {
return interpretations;
};
window.trace_and_sketch = (subject_imagedata)=> {
window.trace_and_sketch = (subject_imagedata) => {
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)
@ -1846,8 +1845,8 @@ window.trace_and_sketch = (subject_imagedata)=> {
// 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 trace_data = ImageTracer.imagedataToTracedata(subject_imagedata, { ltres:1, qtres:0.01, scale:10, /*pal,*/ numberofcolors: 6, });
const {layers} = trace_data;
const trace_data = ImageTracer.imagedataToTracedata(subject_imagedata, { ltres: 1, qtres: 0.01, scale: 10, /*pal,*/ numberofcolors: 6, });
const { layers } = trace_data;
const brush = get_tool_by_id(TOOL_BRUSH);
select_tool(brush);
@ -1855,7 +1854,7 @@ window.trace_and_sketch = (subject_imagedata)=> {
let path_index = 0;
let segment_index = 0;
let active_path;
window.sketching_iid = setInterval(()=> {
window.sketching_iid = setInterval(() => {
const layer = layers[layer_index];
if (!layer) {
clearInterval(window.sketching_iid);
@ -1875,15 +1874,15 @@ window.trace_and_sketch = (subject_imagedata)=> {
brush.pointerup(main_ctx, pointer.x, pointer.y);
return;
}
let {x1, y1, x2, y2} = segment;
let { x1, y1, x2, y2 } = segment;
if (path !== active_path) {
pointer_previous = {x: x1, y: y1};
pointer = {x: x1, y: y1};
pointer_previous = { x: x1, y: y1 };
pointer = { x: x1, y: y1 };
brush.pointerdown(main_ctx, x1, y1);
active_path = path;
}
pointer_previous = {x: x1, y: y1};
pointer = {x: x2, y: y2};
pointer_previous = { x: x1, y: y1 };
pointer = { x: x2, y: y2 };
brush.paint();
pointer_active = true;
pointer_over_canvas = true;
@ -1891,14 +1890,14 @@ window.trace_and_sketch = (subject_imagedata)=> {
segment_index += 1;
}, 20);
};
window.trace_and_sketch_stop = ()=> {
window.trace_and_sketch_stop = () => {
clearInterval(window.sketching_iid);
pointer_active = false;
pointer_over_canvas = false;
};
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?
// detect gradients / spread out histogram at least, and reject based on that
@ -1909,10 +1908,10 @@ function find_clipart_and_sketch(subject_matter) {
}
const img = new Image();
img.crossOrigin = "Anonymous";
img.onerror = ()=> {
img.onerror = () => {
$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
const max_sketch_width = 500;
const max_sketch_height = 500;
@ -1932,7 +1931,7 @@ function find_clipart_and_sketch(subject_matter) {
trace_and_sketch(image_data);
};
img.src = image_url;
}, (error)=> {
}, (error) => {
if (error.code === "no-results") {
$status_text.text(`No clipart found for '${subject_matter}'`);
} else {
@ -1944,12 +1943,12 @@ function find_clipart_and_sketch(subject_matter) {
function find_clipart(query) {
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}`)
.then(response=> response.text())
.then((html)=> {
.then(response => response.text())
.then((html) => {
// handle relative data-src
html = html.replace(
/((?: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
const doc = new DOMParser().parseFromString(html, "text/html");
@ -1962,34 +1961,34 @@ function find_clipart(query) {
window.search_page_$html = $html;
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()
.map((el)=> el.getAttribute("m"))
.map((json)=> {
.map((el) => el.getAttribute("m"))
.map((json) => {
try {
return JSON.parse(json);
} catch(error) {
} catch (error) {
return null;
}
})
.filter((maybe_parsed)=> maybe_parsed && maybe_parsed.murl)
.map(({murl, t})=> ({image_url: murl, title: t || ""}))
.filter((maybe_parsed) => maybe_parsed && maybe_parsed.murl)
.map(({ murl, t }) => ({ image_url: murl, title: t || "" }))
.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)
if (items.length === 0) {
console.log("Fallback to thumbnails");
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);
}
// fallback in case they also change the class for images (this may match totally irrelevant things)
if (items.length === 0) {
console.log("Fallback to most imgs");
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.
.map((el)=> ({image_url: el.src || el.dataset.src, title: ""}))
.filter((el) => !el.closest("[role='navigation'], nav")) // ignore "Related searches", "Refine your search" etc.
.map((el) => ({ image_url: el.src || el.dataset.src, title: "" }))
.filter(validate_item);
}
console.log(`Search results for '${query}':`, items);
@ -2021,7 +2020,7 @@ function clickButtonVisibly(button) {
if (button.matches("button:not(.toggle)")) {
button.style.borderImage = "var(--inset-deep-border-image)";
setTimeout(()=> {
setTimeout(() => {
button.style.borderImage = "";
// delay the button.click() as well, so the pressed state is
// 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;
}
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 = Object.assign({match_text: input_text}, expected);
expected = Object.assign({ match_text: input_text }, expected);
const expected_json = JSON.stringify(expected, null, 4);
const actual_json = JSON.stringify(actual, null, 4);
if (expected_json !== actual_json) {
@ -2073,8 +2072,8 @@ All interpretations:`, interpretations);
const fixed_up_input_text = fix_up_speech_recognition(input_text);
if (fixed_up_input_text !== input_text) {
console.error(`Failed test. Speech recognition fixup changed the input from:
'${input_text}' to:
'${fixed_up_input_text}'`);
'${input_text}' to:
'${fixed_up_input_text}'`);
return;
}
}
@ -2085,9 +2084,9 @@ function test_speech(input_text, expected) {
if (typeof expected === "string") {
if (fixed_up_input_text !== expected) {
console.error(`Failed test. Speech recognition fixup changed the input from:
'${input_text}' to:
'${fixed_up_input_text}' instead of:
'${expected}'`);
'${input_text}' to:
'${fixed_up_input_text}' instead of:
'${expected}'`);
return;
}
} else {
@ -2097,40 +2096,40 @@ function test_speech(input_text, expected) {
function test_speech_recognition() {
// test_command("select blue", {color: "blue"}); // @FIXME
test_command("select fill", {tool_id: TOOL_FILL});
test_command("select text", {tool_id: TOOL_TEXT});
test_command("select", {tool_id: TOOL_SELECT});
test_speech("free form select", {tool_id: TOOL_FREE_FORM_SELECT});
test_speech("lips", {match_text: "ellipse", tool_id: TOOL_ELLIPSE});
test_command("select fill", { tool_id: TOOL_FILL });
test_command("select text", { tool_id: TOOL_TEXT });
test_command("select", { tool_id: TOOL_SELECT });
test_speech("free form select", { tool_id: TOOL_FREE_FORM_SELECT });
test_speech("lips", { match_text: "ellipse", tool_id: TOOL_ELLIPSE });
test_command("", null);
// test_command("I got you some new books", null);
// test_command("pan view sorthweast", null); // currently opens View menu
test_command("1 pixel lines", {size: 1});
test_command("1 pixel wide lines", {size: 1});
test_command("set line width to 5", {size: 5});
test_command("1 pixel lines", { size: 1 });
test_command("1 pixel wide lines", { size: 1 });
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_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_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 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("end", {type: "stop-drawing"});
test_command("stop", {type: "stop-drawing"});
test_command("draw a stop sign", {sketch_subject: "stop sign"});
test_command("end", { type: "stop-drawing" });
test_command("stop", { type: "stop-drawing" });
test_command("draw a stop sign", { sketch_subject: "stop sign" });
test_command("pan view southwest", {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("view 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("scroll down", {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 downwards and 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_command("scroll the view southward", {vector: {x: 0, y: +1}});
test_command("pan view southwest", { 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("view 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("scroll down", { 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 downwards and 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_command("scroll the view southward", { vector: { x: 0, y: +1 } });
}

View File

@ -56,9 +56,9 @@
localStorage[disable_seasonal_theme_key] = "true"; // any theme change disables seasonal theme (unless of course you select the seasonal theme)
grinch_button?.remove();
// eslint-disable-next-line no-empty
} catch(error) {}
} catch (error) { }
const signal_theme_load = ()=> {
const signal_theme_load = () => {
$(window).triggerHandler("theme-load");
$(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);
let img = ChooserCanvas.cache[url];
if(!img){
if (!img) {
img = ChooserCanvas.cache[url] = E("img");
img.onerror = ()=> {
img.onerror = () => {
delete ChooserCanvas.cache[url];
};
img.src = url;
@ -30,7 +30,7 @@ const ChooserCanvas = (
destX, destY, destWidth, destHeight
);
// eslint-disable-next-line no-empty
} catch(error) {}
} catch (error) { }
// if(invert){
// invert_rgb(c.ctx); // can fail due to tainted canvas if running from file: protocol
// }
@ -92,7 +92,7 @@ const ChooserDiv = (
const $Choose = (things, display, choose, is_chosen, gray_background_for_unselected) => {
const $chooser = $(E("div")).addClass("chooser").css("touch-action", "none");
const choose_thing = (thing) => {
if(is_chosen(thing)){
if (is_chosen(thing)) {
return;
}
choose(thing);
@ -103,11 +103,11 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
return;
}
$chooser.empty();
for(let i=0; i<things.length; i++){
for (let i = 0; i < things.length; i++) {
(thing => {
const $option_container = $(E("div")).addClass("chooser-option").appendTo($chooser);
$option_container.data("thing", thing);
const reuse_canvas = (width, height)=> {
const reuse_canvas = (width, height) => {
let option_canvas = $option_container.find("canvas")[0];
if (option_canvas) {
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]);
}
const onpointerover_while_pointer_down = (event)=> {
const onpointerover_while_pointer_down = (event) => {
const option_container = event.target.closest(".chooser-option");
if (option_container) {
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 target = document.elementFromPoint(touch.clientX, touch.clientY);
const option_container = target.closest(".chooser-option");
@ -159,7 +159,7 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
}
event.preventDefault();
};
$chooser.on("pointerdown click", (event)=> {
$chooser.on("pointerdown click", (event) => {
const option_container = event.target.closest(".chooser-option");
if (option_container) {
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);
}
});
$G.on("pointerup pointercancel", ()=> {
$G.on("pointerup pointercancel", () => {
$chooser.off("pointerover", onpointerover_while_pointer_down);
$chooser.off("touchmove", ontouchmove_while_pointer_down);
});
@ -180,11 +180,11 @@ const $Choose = (things, display, choose, is_chosen, gray_background_for_unselec
const $ChooseShapeStyle = () => {
const $chooser = $Choose(
[
{stroke: true, fill: false},
{stroke: true, fill: true},
{stroke: false, fill: true}
{ stroke: true, fill: false },
{ stroke: true, 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_ctx = ss_canvas.ctx;
@ -194,29 +194,29 @@ const $ChooseShapeStyle = () => {
const style = getComputedStyle(ss_canvas);
ss_ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
if(stroke){
if (stroke) {
// just using a solid rectangle for the stroke
// 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
b += 1;
ss_ctx.fillStyle = style.getPropertyValue("--ButtonShadow");
if(fill){
ss_ctx.fillRect(b, b, ss_canvas.width-b*2, ss_canvas.height-b*2);
}else{
ss_ctx.clearRect(b, b, ss_canvas.width-b*2, ss_canvas.height-b*2);
if (fill) {
ss_ctx.fillRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
} else {
ss_ctx.clearRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
}
return ss_canvas;
},
({stroke, fill}) => {
({ stroke, fill }) => {
$chooser.stroke = stroke;
$chooser.fill = fill;
},
({stroke, fill}) => $chooser.stroke === stroke && $chooser.fill === fill
({ stroke, fill }) => $chooser.stroke === stroke && $chooser.fill === fill
).addClass("choose-shape-style");
$chooser.fill = false;
@ -231,9 +231,9 @@ const $choose_brush = $Choose(
const circular_brush_sizes = [7, 4, 1];
const brush_sizes = [8, 5, 2];
const things = [];
brush_shapes.forEach((brush_shape)=> {
brush_shapes.forEach((brush_shape) => {
const sizes = brush_shape === "circle" ? circular_brush_sizes : brush_sizes;
sizes.forEach((brush_size)=> {
sizes.forEach((brush_size) => {
things.push({
shape: brush_shape,
size: brush_size,
@ -254,10 +254,10 @@ const $choose_brush = $Choose(
replace_colors_with_swatch(cb_canvas.ctx, color);
return cb_canvas;
}, ({shape, size}) => {
}, ({ shape, size }) => {
brush_shape = shape;
brush_size = size;
}, ({shape, size}) => brush_shape === shape && brush_size === size
}, ({ shape, size }) => brush_shape === shape && brush_size === size
).addClass("choose-brush");
const $choose_eraser_size = $Choose(
@ -285,7 +285,7 @@ const $choose_stroke_size = $Choose(
const center_y = (h - size) / 2;
const style = getComputedStyle(cs_canvas);
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;
},
size => {
@ -304,11 +304,11 @@ const $choose_magnification = $Choose(
"magnification-option",
is_chosen, // invert if chosen
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
reuse_div,
);
if(secret){
if (secret) {
$(chooser_el).addClass("secret-option");
}
return chooser_el;
@ -319,13 +319,13 @@ const $choose_magnification = $Choose(
scale => scale === magnification,
true,
).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
.find(".secret-option")
.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];
@ -368,7 +368,7 @@ const $choose_transparent_mode = $Choose(
return ChooserDiv(
"transparent-mode-option",
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
b, b, sw, sh, // x, y, width, height on created destination canvas
reuse_div,

View File

@ -1,4 +1,4 @@
(()=> {
(() => {
// This is for linting stuff at the bottom.
/* eslint no-restricted-syntax: ["error", "ThisExpression"] */
@ -49,9 +49,9 @@ window.tools = [{
pointerdown() {
this.x_min = pointer.x;
this.x_max = pointer.x+1;
this.x_max = pointer.x + 1;
this.y_min = pointer.y;
this.y_max = pointer.y+1;
this.y_max = pointer.y + 1;
this.points = [];
this.preview_canvas = make_canvas(main_canvas.width, main_canvas.height);
@ -72,7 +72,7 @@ window.tools = [{
this.y_min = Math.min(pointer.y, this.y_min);
this.y_max = Math.max(pointer.y, this.y_max);
bresenham_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y)=> {
bresenham_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y) => {
this.paint_iteration(x, y);
});
},
@ -85,8 +85,8 @@ window.tools = [{
// Find the dimensions on the canvas of the tiny square to invert
const inversion_size = 2;
const rect_x = ~~(x - inversion_size/2);
const rect_y = ~~(y - inversion_size/2);
const rect_x = ~~(x - inversion_size / 2);
const rect_y = ~~(y - inversion_size / 2);
const rect_w = inversion_size;
const rect_h = inversion_size;
@ -94,11 +94,11 @@ window.tools = [{
const id_src = main_ctx.getImageData(rect_x, rect_y, rect_w, rect_h);
const id_dest = ctx_dest.getImageData(rect_x, rect_y, rect_w, rect_h);
for(let i=0, l=id_dest.data.length; i<l; i+=4){
id_dest.data[i+0] = 255 - id_src.data[i+0];
id_dest.data[i+1] = 255 - id_src.data[i+1];
id_dest.data[i+2] = 255 - id_src.data[i+2];
id_dest.data[i+3] = 255;
for (let i = 0, l = id_dest.data.length; i < l; i += 4) {
id_dest.data[i + 0] = 255 - id_src.data[i + 0];
id_dest.data[i + 1] = 255 - id_src.data[i + 1];
id_dest.data[i + 2] = 255 - id_src.data[i + 2];
id_dest.data[i + 3] = 255;
// @TODO maybe: invert based on id_src.data[i+3] and the checkered background
}
@ -117,7 +117,7 @@ window.tools = [{
this.y_max
);
if(selection){
if (selection) {
// for silly multitools feature
show_error_message("This isn't supposed to happen: Free-Form Select after Select in the tool chain?");
meld_selection_into_canvas();
@ -127,7 +127,7 @@ window.tools = [{
name: localize("Free-Form Select"),
icon: get_icon_for_tool(get_tool_by_id(TOOL_FREE_FORM_SELECT)),
soft: true,
}, ()=> {
}, () => {
selection = new OnCanvasSelection(
this.x_min,
this.y_min,
@ -139,13 +139,13 @@ window.tools = [{
});
},
cancel() {
if(!this.preview_canvas){return;}
if (!this.preview_canvas) { return; }
this.preview_canvas.width = 1;
this.preview_canvas.height = 1;
},
drawPreviewUnderGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
if(!pointer_active && !pointer_over_canvas){return;}
if(!this.preview_canvas){return;}
if (!pointer_active && !pointer_over_canvas) { return; }
if (!this.preview_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -167,12 +167,12 @@ window.tools = [{
selectBox(rect_x, rect_y, rect_width, rect_height) {
if (rect_width > 1 && rect_height > 1) {
var free_form_selection = selection;
if(selection){
if (selection) {
// for silly multitools feature
meld_selection_into_canvas();
}
if (ctrl) {
undoable({name: "Crop"}, () => {
undoable({ name: "Crop" }, () => {
var cropped_canvas = make_canvas(rect_width, rect_height);
cropped_canvas.ctx.drawImage(main_canvas, -rect_x, -rect_y);
main_ctx.copy(cropped_canvas);
@ -225,7 +225,7 @@ window.tools = [{
get_tool_by_id(TOOL_SELECT),
]),
soft: true,
}, ()=> {
}, () => {
selection = new OnCanvasSelection(
x_min,
y_min,
@ -240,7 +240,7 @@ window.tools = [{
name: localize("Select"),
icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
soft: true,
}, ()=> {
}, () => {
selection = new OnCanvasSelection(rect_x, rect_y, rect_width, rect_height);
});
}
@ -263,16 +263,16 @@ window.tools = [{
mask_canvas: null,
get_rect(x, y) {
const rect_x = Math.ceil(x - eraser_size/2);
const rect_y = Math.ceil(y - eraser_size/2);
const rect_x = Math.ceil(x - eraser_size / 2);
const rect_y = Math.ceil(y - eraser_size / 2);
const rect_w = eraser_size;
const rect_h = eraser_size;
return {rect_x, rect_y, rect_w, rect_h};
return { rect_x, rect_y, rect_w, rect_h };
},
drawPreviewUnderGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
if(!pointer_active && !pointer_over_canvas){return;}
const {rect_x, rect_y, rect_w, rect_h} = this.get_rect(x, y);
if (!pointer_active && !pointer_over_canvas) { return; }
const { rect_x, rect_y, rect_w, rect_h } = this.get_rect(x, y);
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -289,20 +289,20 @@ window.tools = [{
ctx.fillRect(rect_x, rect_y, rect_w, rect_h);
},
drawPreviewAboveGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
if(!pointer_active && !pointer_over_canvas){return;}
if (!pointer_active && !pointer_over_canvas) { return; }
const {rect_x, rect_y, rect_w, rect_h} = this.get_rect(x, y);
const { rect_x, rect_y, rect_w, rect_h } = this.get_rect(x, y);
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
const hairline_width = 1/scale;
const hairline_width = 1 / scale;
ctx.strokeStyle = "black";
ctx.lineWidth = hairline_width;
if (grid_visible) {
ctx.strokeRect(rect_x+ctx.lineWidth/2, rect_y+ctx.lineWidth/2, rect_w, rect_h);
ctx.strokeRect(rect_x + ctx.lineWidth / 2, rect_y + ctx.lineWidth / 2, rect_w, rect_h);
} else {
ctx.strokeRect(rect_x+ctx.lineWidth/2, rect_y+ctx.lineWidth/2, rect_w-ctx.lineWidth, rect_h-ctx.lineWidth);
ctx.strokeRect(rect_x + ctx.lineWidth / 2, rect_y + ctx.lineWidth / 2, rect_w - ctx.lineWidth, rect_h - ctx.lineWidth);
}
},
pointerdown() {
@ -326,14 +326,14 @@ window.tools = [{
const h = ctx.canvas.height;
const y = (t % 1) * -h * (n - 1);
const gradient = ctx.createLinearGradient(0, y, 0, y + h * n);
gradient.addColorStop(0/n, "red");
gradient.addColorStop(1/n, "gold");
gradient.addColorStop(2/n, "#00d90b");
gradient.addColorStop(3/n, "#2e64d9");
gradient.addColorStop(4/n, "#8f2ed9");
gradient.addColorStop(0 / n, "red");
gradient.addColorStop(1 / n, "gold");
gradient.addColorStop(2 / n, "#00d90b");
gradient.addColorStop(3 / n, "#2e64d9");
gradient.addColorStop(4 / n, "#8f2ed9");
// last two same as the first two so it can seamlessly wrap
gradient.addColorStop(5/n, "red");
gradient.addColorStop(6/n, "gold");
gradient.addColorStop(5 / n, "red");
gradient.addColorStop(6 / n, "gold");
color = gradient;
}
const mask_fill_canvas = make_canvas(this.mask_canvas);
@ -348,7 +348,7 @@ window.tools = [{
undoable({
name: get_language().match(/^en\b/) ? (this.color_eraser_mode ? "Color Eraser" : "Eraser") : localize("Eraser/Color Eraser"),
icon: get_icon_for_tool(this),
}, ()=> {
}, () => {
this.render_from_mask(main_ctx);
this.mask_canvas = null;
@ -358,20 +358,20 @@ window.tools = [{
this.mask_canvas = null;
},
paint(ctx, x, y) {
bresenham_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y)=> {
bresenham_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y) => {
this.paint_iteration(ctx, x, y);
});
},
paint_iteration(ctx, x, y) {
const {rect_x, rect_y, rect_w, rect_h} = this.get_rect(x, y);
const { rect_x, rect_y, rect_w, rect_h } = this.get_rect(x, y);
this.color_eraser_mode = button !== 0;
if(!this.color_eraser_mode){
if (!this.color_eraser_mode) {
// Eraser
this.mask_canvas.ctx.fillStyle = "white";
this.mask_canvas.ctx.fillRect(rect_x, rect_y, rect_w, rect_h);
}else{
} else {
// Color Eraser
// Right click with the eraser to selectively replace
// the selected foreground color with the selected background color
@ -383,17 +383,17 @@ window.tools = [{
const fill_threshold = 1; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184
for(let i=0, l=test_image_data.data.length; i<l; i+=4){
if(
Math.abs(test_image_data.data[i+0] - fg_rgba[0]) <= fill_threshold &&
Math.abs(test_image_data.data[i+1] - fg_rgba[1]) <= fill_threshold &&
Math.abs(test_image_data.data[i+2] - fg_rgba[2]) <= fill_threshold &&
Math.abs(test_image_data.data[i+3] - fg_rgba[3]) <= fill_threshold
for (let i = 0, l = test_image_data.data.length; i < l; i += 4) {
if (
Math.abs(test_image_data.data[i + 0] - fg_rgba[0]) <= fill_threshold &&
Math.abs(test_image_data.data[i + 1] - fg_rgba[1]) <= fill_threshold &&
Math.abs(test_image_data.data[i + 2] - fg_rgba[2]) <= fill_threshold &&
Math.abs(test_image_data.data[i + 3] - fg_rgba[3]) <= fill_threshold
) {
result_image_data.data[i+0] = 255;
result_image_data.data[i+1] = 255;
result_image_data.data[i+2] = 255;
result_image_data.data[i+3] = 255;
result_image_data.data[i + 0] = 255;
result_image_data.data[i + 1] = 255;
result_image_data.data[i + 2] = 255;
result_image_data.data[i + 3] = 255;
}
}
@ -414,11 +414,11 @@ window.tools = [{
description: "Fills an area with the selected drawing color.",
cursor: ["fill-bucket", [8, 22], "crosshair"],
pointerdown(ctx, x, y) {
if(shift){
if (shift) {
undoable({
name: "Replace Color",
icon: get_icon_for_tool(this),
}, ()=> {
}, () => {
// Perform global color replacement
draw_noncontiguous_fill(ctx, x, y, fill_color);
});
@ -426,7 +426,7 @@ window.tools = [{
undoable({
name: localize("Fill With Color"),
icon: get_icon_for_tool(this),
}, ()=> {
}, () => {
// Perform a normal fill operation
draw_fill(ctx, x, y, fill_color);
});
@ -470,11 +470,11 @@ window.tools = [{
});
},
paint(ctx, x, y) {
if(x >= 0 && y >= 0 && x < main_canvas.width && y < main_canvas.height){
if (x >= 0 && y >= 0 && x < main_canvas.width && y < main_canvas.height) {
const id = ctx.getImageData(~~x, ~~y, 1, 1);
const [r, g, b, a] = id.data;
this.current_color = `rgba(${r},${g},${b},${a/255})`;
}else{
this.current_color = `rgba(${r},${g},${b},${a / 255})`;
} else {
this.current_color = "white";
}
this.display_current_color();
@ -496,20 +496,20 @@ window.tools = [{
cursor: ["magnifier", [16, 16], "zoom-in"], // overridden below
deselect: true,
getProspectiveMagnification: ()=> (
getProspectiveMagnification: () => (
magnification === 1 ? return_to_magnification : 1
),
drawPreviewAboveGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
if(!pointer_active && !pointer_over_canvas){return;}
if(pointer_active) { return; }
if (!pointer_active && !pointer_over_canvas) { return; }
if (pointer_active) { return; }
const prospective_magnification = this.getProspectiveMagnification();
// hacky place to put this but whatever
// use specific zoom-in/zoom-out as fallback,
// even though the custom cursor image is less descriptive
// because there's no generic "zoom" css cursor
if(prospective_magnification < magnification) {
if (prospective_magnification < magnification) {
$canvas.css({
cursor: make_css_cursor("magnifier", [16, 16], "zoom-out"),
});
@ -519,14 +519,14 @@ window.tools = [{
});
}
if(prospective_magnification < magnification) { return; } // hide if would be zooming out
if (prospective_magnification < magnification) { return; } // hide if would be zooming out
// prospective viewport size in document coords
const w = $canvas_area.width() / prospective_magnification;
const h = $canvas_area.height() / prospective_magnification;
let rect_x1 = ~~(x - w/2);
let rect_y1 = ~~(y - h/2);
let rect_x1 = ~~(x - w / 2);
let rect_y1 = ~~(y - h / 2);
// try to move rect into bounds without squishing
rect_x1 = Math.max(0, rect_x1);
@ -548,28 +548,28 @@ window.tools = [{
const rect_x = rect_x1;
const rect_y = rect_y1;
const id_src = main_canvas.ctx.getImageData(rect_x, rect_y, rect_w+1, rect_h+1);
const id_dest = ctx.getImageData((rect_x+translate_x)*scale, (rect_y+translate_y)*scale, rect_w*scale+1, rect_h*scale+1);
const id_src = main_canvas.ctx.getImageData(rect_x, rect_y, rect_w + 1, rect_h + 1);
const id_dest = ctx.getImageData((rect_x + translate_x) * scale, (rect_y + translate_y) * scale, rect_w * scale + 1, rect_h * scale + 1);
function copyPixelInverted(x_dest, y_dest) {
const x_src = ~~(x_dest / scale);
const y_src = ~~(y_dest / scale);
const index_src = (x_src + y_src * id_src.width) * 4;
const index_dest = (x_dest + y_dest * id_dest.width) * 4;
id_dest.data[index_dest+0] = 255 - id_src.data[index_src+0];
id_dest.data[index_dest+1] = 255 - id_src.data[index_src+1];
id_dest.data[index_dest+2] = 255 - id_src.data[index_src+2];
id_dest.data[index_dest+3] = 255;
id_dest.data[index_dest + 0] = 255 - id_src.data[index_src + 0];
id_dest.data[index_dest + 1] = 255 - id_src.data[index_src + 1];
id_dest.data[index_dest + 2] = 255 - id_src.data[index_src + 2];
id_dest.data[index_dest + 3] = 255;
// @TODO maybe: invert based on id_src.data[index_src+3] and the checkered background
}
for(let x=0, limit=id_dest.width; x<limit; x+=1){
for (let x = 0, limit = id_dest.width; x < limit; x += 1) {
copyPixelInverted(x, 0);
copyPixelInverted(x, id_dest.height-1);
copyPixelInverted(x, id_dest.height - 1);
}
for(let y=1, limit=id_dest.height-1; y<limit; y+=1){
for (let y = 1, limit = id_dest.height - 1; y < limit; y += 1) {
copyPixelInverted(0, y);
copyPixelInverted(id_dest.width-1, y);
copyPixelInverted(id_dest.width - 1, y);
}
// for debug: fill rect
@ -579,7 +579,7 @@ window.tools = [{
// }
// }
ctx.putImageData(id_dest, (rect_x+translate_x)*scale, (rect_y+translate_y)*scale);
ctx.putImageData(id_dest, (rect_x + translate_x) * scale, (rect_y + translate_y) * scale);
// debug:
// ctx.scale(scale, scale);
@ -599,7 +599,7 @@ window.tools = [{
const w = $canvas_area.width() / magnification;
const h = $canvas_area.height() / magnification;
$canvas_area.scrollLeft((x - w/2) * magnification / prev_magnification);
$canvas_area.scrollLeft((x - w / 2) * magnification / prev_magnification);
// Nevermind, canvas, isn't aligned to the right in RTL layout!
// if (get_direction() === "rtl") {
// // scrollLeft coordinates can be negative for RTL
@ -607,7 +607,7 @@ window.tools = [{
// } else {
// $canvas_area.scrollLeft((x - w/2) * magnification / prev_magnification);
// }
$canvas_area.scrollTop((y - h/2) * magnification / prev_magnification);
$canvas_area.scrollTop((y - h / 2) * magnification / prev_magnification);
$canvas_area.trigger("scroll");
}
},
@ -623,7 +623,7 @@ window.tools = [{
cursor: ["pencil", [13, 23], "crosshair"],
stroke_only: true,
get_brush() {
return {size: pencil_size, shape: "circle"};
return { size: pencil_size, shape: "circle" };
}
}, {
id: TOOL_BRUSH,
@ -641,7 +641,7 @@ window.tools = [{
cursor: ["precise-dotted", [16, 16], "crosshair"],
dynamic_preview_cursor: true,
get_brush() {
return {size: brush_size, shape: brush_shape};
return { size: brush_size, shape: brush_shape };
},
$options: $choose_brush
}, {
@ -659,11 +659,11 @@ window.tools = [{
paint_on_time_interval: 5,
paint_mask(ctx, x, y) {
const r = airbrush_size / 2;
for(let i = 0; i < 6 + r/5; i++){
const rx = (Math.random()*2-1) * r;
const ry = (Math.random()*2-1) * r;
const d = rx*rx + ry*ry;
if(d <= r * r){
for (let i = 0; i < 6 + r / 5; i++) {
const rx = (Math.random() * 2 - 1) * r;
const ry = (Math.random() * 2 - 1) * r;
const d = rx * rx + ry * ry;
if (d <= r * r) {
ctx.fillRect(x + ~~rx, y + ~~ry, 1, 1);
}
}
@ -701,7 +701,7 @@ window.tools = [{
stroke_only: true,
shape(ctx, x, y, w, h) {
update_brush_for_drawing_lines(stroke_size);
draw_line(ctx, x, y, x+w, y+h, stroke_size);
draw_line(ctx, x, y, x + w, y + h, stroke_size);
},
$options: $choose_stroke_size
}, {
@ -718,30 +718,30 @@ window.tools = [{
points: [],
preview_canvas: null,
pointerup(ctx, x, y) {
if(this.points.length >= 4){
if (this.points.length >= 4) {
undoable({
name: localize("Curve"),
icon: get_icon_for_tool(this),
}, ()=> {
}, () => {
ctx.drawImage(this.preview_canvas, 0, 0);
});
this.points = [];
}
},
pointerdown(ctx, x, y) {
if(this.points.length < 1){
if (this.points.length < 1) {
this.preview_canvas = make_canvas(main_canvas.width, main_canvas.height);
this.points.push({x, y});
this.points.push({ x, y });
if (!$("body").hasClass("eye-gaze-mode")) {
// second point so first action draws a line
this.points.push({x, y});
this.points.push({ x, y });
}
}else{
this.points.push({x, y});
} else {
this.points.push({ x, y });
}
},
paint(ctx, x, y) {
if(this.points.length < 1){ return; }
if (this.points.length < 1) { return; }
update_brush_for_drawing_lines(stroke_size);
@ -753,7 +753,7 @@ window.tools = [{
this.preview_canvas.ctx.strokeStyle = stroke_color;
// Draw curves on preview canvas
if(this.points.length === 4){
if (this.points.length === 4) {
draw_bezier_curve(
this.preview_canvas.ctx,
this.points[0].x, this.points[0].y,
@ -762,7 +762,7 @@ window.tools = [{
this.points[1].x, this.points[1].y,
stroke_size
);
}else if(this.points.length === 3){
} else if (this.points.length === 3) {
draw_quadratic_curve(
this.preview_canvas.ctx,
this.points[0].x, this.points[0].y,
@ -770,14 +770,14 @@ window.tools = [{
this.points[1].x, this.points[1].y,
stroke_size
);
}else if(this.points.length === 2){
} else if (this.points.length === 2) {
draw_line(
this.preview_canvas.ctx,
this.points[0].x, this.points[0].y,
this.points[1].x, this.points[1].y,
stroke_size
);
}else{
} else {
draw_line(
this.preview_canvas.ctx,
this.points[0].x, this.points[0].y,
@ -788,7 +788,7 @@ window.tools = [{
},
drawPreviewUnderGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
// if(!pointer_active && !pointer_over_canvas){return;}
if(!this.preview_canvas){return;}
if (!this.preview_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -824,19 +824,19 @@ window.tools = [{
description: localize("Draws a rectangle with the selected fill style."),
cursor: ["precise", [16, 16], "crosshair"],
shape(ctx, x, y, w, h) {
if(w < 0){ x += w; w = -w; }
if(h < 0){ y += h; h = -h; }
if (w < 0) { x += w; w = -w; }
if (h < 0) { y += h; h = -h; }
if(this.$options.fill){
if (this.$options.fill) {
ctx.fillRect(x, y, w, h);
}
if(this.$options.stroke){
if(w < stroke_size * 2 || h < stroke_size * 2){
if (this.$options.stroke) {
if (w < stroke_size * 2 || h < stroke_size * 2) {
ctx.save();
ctx.fillStyle = ctx.strokeStyle;
ctx.fillRect(x, y, w, h);
ctx.restore();
}else{
} else {
// @TODO: shouldn't that be ~~(stroke_size / 2)?
ctx.strokeRect(x + stroke_size / 2, y + stroke_size / 2, w - stroke_size, h - stroke_size);
}
@ -860,8 +860,8 @@ window.tools = [{
// Record the last click for double-clicking
// A double click happens on pointerdown of a second click
// (within a cylindrical volume in 2d space + 1d time)
last_click_pointerdown: {x: -Infinity, y: -Infinity, time: -Infinity},
last_click_pointerup: {x: -Infinity, y: -Infinity, time: -Infinity},
last_click_pointerdown: { x: -Infinity, y: -Infinity, time: -Infinity },
last_click_pointerup: { x: -Infinity, y: -Infinity, time: -Infinity },
// The vertices of the polygon
points: [],
@ -870,58 +870,58 @@ window.tools = [{
preview_canvas: null,
pointerup(ctx, x, y) {
if(this.points.length < 1){ return; }
if (this.points.length < 1) { return; }
const i = this.points.length - 1;
this.points[i].x = x;
this.points[i].y = y;
const dx = this.points[i].x - this.points[0].x;
const dy = this.points[i].y - this.points[0].y;
const d = Math.sqrt(dx*dx + dy*dy);
if($("body").hasClass("eye-gaze-mode")){
if(this.points.length >= 3){
if(d < stroke_size * 10 + 20){
const d = Math.sqrt(dx * dx + dy * dy);
if ($("body").hasClass("eye-gaze-mode")) {
if (this.points.length >= 3) {
if (d < stroke_size * 10 + 20) {
this.complete(ctx);
}
}
}else{
if(d < stroke_size * 5.1010101){ // arbitrary number (@TODO: find correct value (or formula))
} else {
if (d < stroke_size * 5.1010101) { // arbitrary number (@TODO: find correct value (or formula))
this.complete(ctx);
}
}
this.last_click_pointerup = {x, y, time: +(new Date)};
this.last_click_pointerup = { x, y, time: +(new Date) };
},
pointerdown(ctx, x, y) {
if(this.points.length < 1){
if (this.points.length < 1) {
this.preview_canvas = make_canvas(main_canvas.width, main_canvas.height);
// Add the first point of the polygon
this.points.push({x, y});
this.points.push({ x, y });
if (!$("body").hasClass("eye-gaze-mode")) {
// Add a second point so first action draws a line
this.points.push({x, y});
this.points.push({ x, y });
}
}else{
} else {
const lx = this.last_click_pointerdown.x;
const ly = this.last_click_pointerdown.y;
const lt = this.last_click_pointerdown.time;
const dx = x - lx;
const dy = y - ly;
const dt = +(new Date) - lt;
const d = Math.sqrt(dx*dx + dy*dy);
if(d < 4.1010101 && dt < 250){ // arbitrary 101 (@TODO: find correct value (or formula))
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 4.1010101 && dt < 250) { // arbitrary 101 (@TODO: find correct value (or formula))
this.complete(ctx);
}else{
} else {
// Add the point
this.points.push({x, y});
this.points.push({ x, y });
}
}
this.last_click_pointerdown = {x, y, time: +new Date};
this.last_click_pointerdown = { x, y, time: +new Date };
},
paint(ctx, x, y) {
if(this.points.length < 1){ return; }
if (this.points.length < 1) { return; }
const i = this.points.length - 1;
this.points[i].x = x;
@ -939,7 +939,7 @@ window.tools = [{
this.points
);
stroke_size = orig_stroke_size;
} else if(this.points.length > 1) {
} else if (this.points.length > 1) {
this.preview_canvas.ctx.strokeStyle = stroke_color;
draw_line_strip(
this.preview_canvas.ctx,
@ -956,7 +956,7 @@ window.tools = [{
},
drawPreviewUnderGrid(ctx, x, y, grid_visible, scale, translate_x, translate_y) {
// if(!pointer_active && !pointer_over_canvas){return;}
if(!this.preview_canvas){return;}
if (!this.preview_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -968,7 +968,7 @@ window.tools = [{
undoable({
name: localize("Polygon"),
icon: get_icon_for_tool(this),
}, ()=> {
}, () => {
ctx.fillStyle = fill_color;
ctx.strokeStyle = stroke_color;
@ -1000,10 +1000,10 @@ window.tools = [{
},
reset() {
this.points = [];
this.last_click_pointerdown = {x: -Infinity, y: -Infinity, time: -Infinity};
this.last_click_pointerup = {x: -Infinity, y: -Infinity, time: -Infinity};
this.last_click_pointerdown = { x: -Infinity, y: -Infinity, time: -Infinity };
this.last_click_pointerup = { x: -Infinity, y: -Infinity, time: -Infinity };
if(!this.preview_canvas){return;}
if (!this.preview_canvas) { return; }
this.preview_canvas.width = 1;
this.preview_canvas.height = 1;
},
@ -1020,13 +1020,13 @@ window.tools = [{
description: localize("Draws an ellipse with the selected fill style."),
cursor: ["precise", [16, 16], "crosshair"],
shape(ctx, x, y, w, h) {
if(w < 0){ x += w; w = -w; }
if(h < 0){ y += h; h = -h; }
if (w < 0) { x += w; w = -w; }
if (h < 0) { y += h; h = -h; }
if(w < stroke_size || h < stroke_size){
if (w < stroke_size || h < stroke_size) {
ctx.fillStyle = ctx.strokeStyle;
draw_ellipse(ctx, x, y, w, h, false, true);
}else{
} else {
draw_ellipse(
ctx,
x + ~~(stroke_size / 2),
@ -1077,12 +1077,12 @@ window.tools = [{
description: localize("Draws a rounded rectangle with the selected fill style."),
cursor: ["precise", [16, 16], "crosshair"],
shape(ctx, x, y, w, h) {
if(w < 0){ x += w; w = -w; }
if(h < 0){ y += h; h = -h; }
if (w < 0) { x += w; w = -w; }
if (h < 0) { y += h; h = -h; }
if(w < stroke_size || h < stroke_size){
if (w < stroke_size || h < stroke_size) {
ctx.fillStyle = ctx.strokeStyle;
const radius = Math.min(8, w/2, h/2);
const radius = Math.min(8, w / 2, h / 2);
// const radius_x = Math.min(8, w/2);
// const radius_y = Math.min(8, h/2);
draw_rounded_rectangle(
@ -1093,8 +1093,8 @@ window.tools = [{
false,
true
);
}else{
const radius = Math.min(8, (w - stroke_size)/2, (h - stroke_size)/2);
} else {
const radius = Math.min(8, (w - stroke_size) / 2, (h - stroke_size) / 2);
// const radius_x = Math.min(8, (w - stroke_size)/2);
// const radius_y = Math.min(8, (h - stroke_size)/2);
draw_rounded_rectangle(
@ -1115,7 +1115,7 @@ window.tools = [{
/* eslint-enable no-restricted-syntax */
tools.forEach((tool)=> {
tools.forEach((tool) => {
if (tool.selectBox) {
let drag_start_x = 0;
let drag_start_y = 0;
@ -1125,37 +1125,37 @@ tools.forEach((tool)=> {
let rect_width = 0;
let rect_height = 0;
tool.pointerdown = ()=> {
tool.pointerdown = () => {
drag_start_x = pointer.x;
drag_start_y = pointer.y;
pointer_has_moved = false;
$G.one("pointermove", ()=> {
$G.one("pointermove", () => {
pointer_has_moved = true;
});
if(selection){
if (selection) {
meld_selection_into_canvas();
}
if(textbox){
if (textbox) {
meld_textbox_into_canvas();
}
canvas_handles.hide();
};
tool.paint = ()=> {
tool.paint = () => {
rect_x = ~~Math.max(0, Math.min(drag_start_x, pointer.x));
rect_y = ~~Math.max(0, Math.min(drag_start_y, pointer.y));
rect_width = (~~Math.min(main_canvas.width, Math.max(drag_start_x, pointer.x) + 1)) - rect_x;
rect_height = (~~Math.min(main_canvas.height, Math.max(drag_start_y, pointer.y + 1))) - rect_y;
};
tool.pointerup = ()=> {
tool.pointerup = () => {
canvas_handles.show();
tool.selectBox(rect_x, rect_y, rect_width, rect_height);
};
tool.cancel = ()=> {
tool.cancel = () => {
canvas_handles.show();
};
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y)=> {
if(!pointer_active){ return; }
if(!pointer_has_moved) { return; }
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y) => {
if (!pointer_active) { return; }
if (!pointer_has_moved) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -1163,38 +1163,38 @@ tools.forEach((tool)=> {
// make the document canvas part of the helper canvas so that inversion can apply to it
ctx.drawImage(main_canvas, 0, 0);
};
tool.drawPreviewAboveGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y)=> {
if(!pointer_active){ return; }
if(!pointer_has_moved) { return; }
tool.drawPreviewAboveGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y) => {
if (!pointer_active) { return; }
if (!pointer_has_moved) { return; }
draw_selection_box(ctx, rect_x, rect_y, rect_width, rect_height, scale, translate_x, translate_y);
};
}
if (tool.shape) {
tool.shape_canvas = null;
tool.pointerdown = ()=> {
tool.pointerdown = () => {
tool.shape_canvas = make_canvas(main_canvas.width, main_canvas.height);
};
tool.paint = ()=> {
tool.paint = () => {
tool.shape_canvas.ctx.clearRect(0, 0, tool.shape_canvas.width, tool.shape_canvas.height);
tool.shape_canvas.ctx.fillStyle = main_ctx.fillStyle;
tool.shape_canvas.ctx.strokeStyle = main_ctx.strokeStyle;
tool.shape_canvas.ctx.lineWidth = main_ctx.lineWidth;
tool.shape(tool.shape_canvas.ctx, pointer_start.x, pointer_start.y, pointer.x-pointer_start.x, pointer.y-pointer_start.y);
tool.shape(tool.shape_canvas.ctx, pointer_start.x, pointer_start.y, pointer.x - pointer_start.x, pointer.y - pointer_start.y);
};
tool.pointerup = ()=> {
if(!tool.shape_canvas){ return; }
tool.pointerup = () => {
if (!tool.shape_canvas) { return; }
undoable({
name: tool.name,
icon: get_icon_for_tool(tool),
}, ()=> {
}, () => {
main_ctx.drawImage(tool.shape_canvas, 0, 0);
tool.shape_canvas = null;
});
};
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y)=> {
if(!pointer_active){ return; }
if(!tool.shape_canvas){ return; }
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y) => {
if (!pointer_active) { return; }
if (!tool.shape_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -1207,7 +1207,7 @@ tools.forEach((tool)=> {
// binary mask of the drawn area, either opaque white or transparent
tool.mask_canvas = null;
tool.pointerdown = (ctx, x, y)=> {
tool.pointerdown = (ctx, x, y) => {
if (!tool.mask_canvas) {
tool.mask_canvas = make_canvas(main_canvas.width, main_canvas.height);
}
@ -1219,30 +1219,30 @@ tools.forEach((tool)=> {
}
tool.mask_canvas.ctx.disable_image_smoothing();
};
tool.pointerup = ()=> {
tool.pointerup = () => {
if (!tool.mask_canvas) {
return; // not sure why this would happen per se
}
undoable({
name: tool.name,
icon: get_icon_for_tool(tool),
}, ()=> {
}, () => {
tool.render_from_mask(main_ctx);
tool.mask_canvas.width = 1;
tool.mask_canvas.height = 1;
});
};
tool.paint = (ctx, x, y)=> {
tool.paint = (ctx, x, y) => {
tool.paint_mask(tool.mask_canvas.ctx, x, y);
};
tool.cancel = ()=> {
tool.cancel = () => {
if (tool.mask_canvas) {
tool.mask_canvas.width = 1;
tool.mask_canvas.height = 1;
}
};
tool.render_from_mask = (ctx, previewing)=> { // could be private
tool.render_from_mask = (ctx, previewing) => { // could be private
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(tool.mask_canvas, 0, 0);
@ -1264,14 +1264,14 @@ tools.forEach((tool)=> {
const h = ctx.canvas.height;
const y = (t % 1) * -h * (n - 1);
const gradient = ctx.createLinearGradient(0, y, 0, y + h * n);
gradient.addColorStop(0/n, "red");
gradient.addColorStop(1/n, "gold");
gradient.addColorStop(2/n, "#00d90b");
gradient.addColorStop(3/n, "#2e64d9");
gradient.addColorStop(4/n, "#8f2ed9");
gradient.addColorStop(0 / n, "red");
gradient.addColorStop(1 / n, "gold");
gradient.addColorStop(2 / n, "#00d90b");
gradient.addColorStop(3 / n, "#2e64d9");
gradient.addColorStop(4 / n, "#8f2ed9");
// last two same as the first two so it can seamlessly wrap
gradient.addColorStop(5/n, "red");
gradient.addColorStop(6/n, "gold");
gradient.addColorStop(5 / n, "red");
gradient.addColorStop(6 / n, "gold");
color = gradient;
}
// @TODO: perf: keep this canvas around too
@ -1280,8 +1280,8 @@ tools.forEach((tool)=> {
ctx.drawImage(mask_fill_canvas, 0, 0);
return translucent;
};
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y)=> {
if(!pointer_active && !pointer_over_canvas){return;}
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y) => {
if (!pointer_active && !pointer_over_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);
@ -1299,7 +1299,7 @@ tools.forEach((tool)=> {
// binary mask of the drawn area, either opaque white or transparent
tool.mask_canvas = null;
tool.init_mask_canvas = (ctx, x, y)=> {
tool.init_mask_canvas = (ctx, x, y) => {
if (!tool.mask_canvas) {
tool.mask_canvas = make_canvas(main_canvas.width, main_canvas.height);
}
@ -1311,14 +1311,14 @@ tools.forEach((tool)=> {
}
tool.mask_canvas.ctx.disable_image_smoothing();
};
tool.pointerdown = (ctx, x, y)=> {
tool.pointerdown = (ctx, x, y) => {
tool.init_mask_canvas();
};
tool.pointerup = ()=> {
tool.pointerup = () => {
undoable({
name: tool.name,
icon: get_icon_for_tool(tool),
}, ()=> {
}, () => {
tool.render_from_mask(main_ctx);
tool.mask_canvas.width = 1;
@ -1326,12 +1326,12 @@ tools.forEach((tool)=> {
});
};
tool.paint = ()=> {
tool.paint = () => {
const brush = tool.get_brush();
const circumference_points = get_circumference_points_for_brush(brush.shape, brush.size);
tool.mask_canvas.ctx.fillStyle = stroke_color;
const iterate_line = brush.size > 1 ? bresenham_dense_line : bresenham_line;
iterate_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y)=> {
iterate_line(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y) => {
for (const point of circumference_points) {
tool.mask_canvas.ctx.fillRect(x + point.x, y + point.y, 1, 1);
}
@ -1340,13 +1340,13 @@ tools.forEach((tool)=> {
stamp_brush_canvas(tool.mask_canvas.ctx, pointer.x, pointer.y, brush.shape, brush.size);
};
tool.cancel = ()=> {
tool.cancel = () => {
if (tool.mask_canvas) {
tool.mask_canvas.width = 1;
tool.mask_canvas.height = 1;
}
};
tool.render_from_mask = (ctx, previewing)=> { // could be private
tool.render_from_mask = (ctx, previewing) => { // could be private
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(tool.mask_canvas, 0, 0);
@ -1368,14 +1368,14 @@ tools.forEach((tool)=> {
const h = ctx.canvas.height;
const y = (t % 1) * -h * (n - 1);
const gradient = ctx.createLinearGradient(0, y, 0, y + h * n);
gradient.addColorStop(0/n, "red");
gradient.addColorStop(1/n, "gold");
gradient.addColorStop(2/n, "#00d90b");
gradient.addColorStop(3/n, "#2e64d9");
gradient.addColorStop(4/n, "#8f2ed9");
gradient.addColorStop(0 / n, "red");
gradient.addColorStop(1 / n, "gold");
gradient.addColorStop(2 / n, "#00d90b");
gradient.addColorStop(3 / n, "#2e64d9");
gradient.addColorStop(4 / n, "#8f2ed9");
// last two same as the first two so it can seamlessly wrap
gradient.addColorStop(5/n, "red");
gradient.addColorStop(6/n, "gold");
gradient.addColorStop(5 / n, "red");
gradient.addColorStop(6 / n, "gold");
color = gradient;
}
// @TODO: perf: keep this canvas around too
@ -1390,8 +1390,8 @@ tools.forEach((tool)=> {
ctx.drawImage(mask_fill_canvas, 0, 0);
return translucent;
};
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y)=> {
if(!pointer_active && !pointer_over_canvas){return;}
tool.drawPreviewUnderGrid = (ctx, x, y, grid_visible, scale, translate_x, translate_y) => {
if (!pointer_active && !pointer_over_canvas) { return; }
ctx.scale(scale, scale);
ctx.translate(translate_x, translate_y);

View File

@ -30,7 +30,7 @@
cancelAnimationFrame(rAF_ID);
$(rotologo).remove();
$window.css({transform: ""});
$window.css({ transform: "" });
removeEventListener("keydown", space_phase_key_handler);
if (player) {
@ -71,14 +71,11 @@
$(rotologo).css({
transform:
`perspective(4000px) rotateY(${
Math.sin(Date.now() / 5000)
}turn) rotateX(${
0
`perspective(4000px) rotateY(${Math.sin(Date.now() / 5000)
}turn) rotateX(${0
}turn) translate(-50%, -50%) translateZ(500px)`,
filter:
`hue-rotate(${
Math.sin(Date.now() / 4000)
`hue-rotate(${Math.sin(Date.now() / 4000)
// @TODO: slow down and stop when you pause
}turn)`,
});
@ -95,10 +92,8 @@
$window.css({
transform:
`perspective(4000px) rotateY(${
-(offsetLeft + ($window.outerWidth() - parent.innerWidth) / 2) / parent.innerWidth / 3
}turn) rotateX(${
(offsetTop + ($window.outerHeight() - parent.innerHeight) / 2) / parent.innerHeight / 3
`perspective(4000px) rotateY(${-(offsetLeft + ($window.outerWidth() - parent.innerWidth) / 2) / parent.innerWidth / 3
}turn) rotateX(${(offsetTop + ($window.outerHeight() - parent.innerHeight) / 2) / parent.innerHeight / 3
}turn)`,
transformOrigin: "50% 50%",
transformStyle: "preserve-3d",
@ -156,7 +151,7 @@
if (event.data == YT.PlayerState.PLAYING) {
// @TODO: pause and resume this timer with the video
setTimeout(() => {
$(rotologo).css({opacity: 1});
$(rotologo).css({ opacity: 1 });
}, 14150);
}
if (event.data == YT.PlayerState.ENDED) {
@ -167,7 +162,7 @@
// 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
// also fade out the rotologo earlier
$(rotologo).css({opacity: 0});
$(rotologo).css({ opacity: 0 });
// destroy rotologo once faded out
setTimeout(stop_vaporwave, 1200);
}
@ -183,13 +178,13 @@
is_theoretically_playing = false;
$(player.getIframe())
.add(rotologo)
.css({opacity: "0"});
.css({ opacity: "0" });
} else {
player.playVideo();
is_theoretically_playing = true;
$(player.getIframe())
.add(rotologo)
.css({opacity: ""});
.css({ opacity: "" });
}
e.preventDefault();
// player.getIframe().focus();
@ -207,7 +202,7 @@
};
addEventListener("keydown", Konami.code(toggle_vaporwave));
addEventListener("keydown", (event)=> {
addEventListener("keydown", (event) => {
if (event.key === "Escape") {
stop_vaporwave();
}

View File

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