392 lines
18 KiB
HTML
392 lines
18 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>JS Paint — MS Paint online</title>
|
|
|
|
<!-- <meta http-equiv="Content-Security-Policy" content="
|
|
default-src 'self';
|
|
img-src 'self' data: blob: http: https:;
|
|
"> -->
|
|
|
|
<link href="styles/normalize.css" rel="stylesheet" type="text/css">
|
|
<link href="styles/layout.css" class="flippable-layout-stylesheet" rel="stylesheet" type="text/css">
|
|
<!-- <link href="styles/print.css" rel="stylesheet" type="text/css" media="print"> -->
|
|
<link href="lib/os-gui/layout.css" class="flippable-layout-stylesheet" 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. -->
|
|
<!-- <link rel="icon" type="image/png" sizes="192x192" href="images/icons/192x192.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/32x32.png">
|
|
<link rel="icon" type="image/png" sizes="96x96" href="images/icons/96x96.png"> -->
|
|
<!-- <link rel="icon" type="image/png" sizes="16x16" href="images/icons/16x16.png"> -->
|
|
<link rel="shortcut icon" href="favicon.ico">
|
|
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg" color="red">
|
|
<link rel="manifest" href="manifest.webmanifest">
|
|
<meta name="msapplication-TileColor" content="#008080">
|
|
<meta name="msapplication-TileImage" content="images/icons/ms-icon-144x144.png">
|
|
<meta name="theme-color" content="#000080">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<meta name="description" content="Classic MS Paint in the browser, with extra features">
|
|
<meta property="og:image:width" content="279">
|
|
<meta property="og:image:height" content="279">
|
|
<meta property="og:description" content="Classic MS Paint in the browser, with extra features.">
|
|
<meta property="og:title" content="JS Paint">
|
|
<meta property="og:url" content="https://jspaint.app">
|
|
<meta property="og:image" content="https://jspaint.app/images/icons/og-image-279x279.jpg">
|
|
<meta name="twitter:title" content="JS Paint">
|
|
<meta name="twitter:description" content="Classic MS Paint in the browser, with extra features">
|
|
<meta name="twitter:image" content="https://jspaint.app/images/meta/twitter-card-plz-no-crop.png">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:site" content="@isaiahodhner">
|
|
<meta name="twitter:creator" content="@isaiahodhner">
|
|
|
|
<link rel="stylesheet" href="styles/themes/classic.css">
|
|
<link rel="stylesheet" href="styles/about.css">
|
|
</head>
|
|
|
|
<body>
|
|
<section id="main-section">
|
|
<h1 style="text-align: center;">
|
|
<a href="https://jspaint.app" target="_blank">
|
|
<img src="images/icons/jspaint.svg" width="192" height="192" alt="">
|
|
<br>
|
|
<span id="jspaint-project-name">JS Paint</span>
|
|
</a>
|
|
</h1>
|
|
|
|
<p style="text-align: center;">JS Paint is a pixel-perfect remake of Microsoft Paint that runs in the browser.
|
|
</p>
|
|
|
|
<p>
|
|
<a href="https://github.com/1j01/jspaint/blob/master/LICENSE.txt" target="_blank"><img
|
|
src="images/about/free.gif" width="88" height="39" alt="Free" style="vertical-align: middle"></a>
|
|
Open source under the permissive
|
|
<a href="https://github.com/1j01/jspaint/blob/master/LICENSE.txt" target="_blank">MIT License</a>.
|
|
</p>
|
|
<p>
|
|
<a href="https://github.com/1j01/jspaint/issues" target="_blank"><img src="images/about/foco.gif" width="40"
|
|
height="40" alt="Ideas" style="vertical-align: middle"></a>
|
|
Request features and report bugs <a href="https://github.com/1j01/jspaint/issues" target="_blank">on
|
|
GitHub</a>
|
|
or <a href="mailto:isaiahodhner@gmail.com?subject=JS%20Paint" target="_blank">by email</a>.
|
|
</p>
|
|
|
|
<b>THIS SITE IS...</b>
|
|
<img src="images/about/conpaint.gif" width="350" height="50" alt="Under Construction"
|
|
style="vertical-align: middle">
|
|
|
|
<img src=" images/about/red_paint_bucket_brush_md_clr_24887PURPLE.gif" width="87" height="100"
|
|
alt="Paint bucket" style="float: right">
|
|
|
|
<!-- <iframe src="index.html" title="JS Paint app" id="jspaint-iframe"></iframe> -->
|
|
|
|
<div class="window os-window focused" id="os-window-jspaint"
|
|
style="width: 267px; height: 392px; touch-action: none; position: relative; z-index: 9; display: flex;">
|
|
<div class="window-titlebar" style="touch-action: none;"><img src="images/icons/16x16.png" width="16"
|
|
height="16" draggable="false" style="width: 16px; height: 16px;" alt="">
|
|
<div class="window-title-area"><span class="window-title">untitled - Paint</span></div><button
|
|
class="window-minimize-button window-action-minimize window-button"
|
|
aria-label="Minimize window"><span class="window-button-icon"></span></button><button
|
|
class="window-maximize-button window-action-maximize window-button"
|
|
aria-label="Maximize or restore window"><span class="window-button-icon"></span></button><button
|
|
class="window-close-button window-action-close window-button" aria-label="Close window"><span
|
|
class="window-button-icon"></span></button>
|
|
</div>
|
|
<div class="window-content" tabindex="-1" style="outline: none; display: flex; flex-direction: column;">
|
|
<!-- <iframe id="jspaint-iframe" src="index.html" -->
|
|
<iframe id="jspaint-iframe" src="/"
|
|
style="min-width: 0px; min-height: 0px; flex: 1 1 1px; border: 0px;"></iframe>
|
|
</div>
|
|
<!-- <div class="handle"
|
|
style="position: absolute; top: -2px; right: -2px; width: 4px; height: 4px; touch-action: none; cursor: ne-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; top: -2px; left: calc(2px); width: calc(100% - 8px + 4px); height: 4px; touch-action: none; cursor: n-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; top: -2px; left: -2px; width: 4px; height: 4px; touch-action: none; cursor: nw-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; top: calc(2px); left: -2px; width: 4px; height: calc(100% - 8px + 4px); touch-action: none; cursor: w-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; bottom: -2px; left: -2px; width: 4px; height: 4px; touch-action: none; cursor: sw-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; bottom: -2px; left: calc(2px); width: calc(100% - 8px + 4px); height: 4px; touch-action: none; cursor: s-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; bottom: -2px; right: -2px; width: 4px; height: 4px; touch-action: none; cursor: se-resize;">
|
|
</div>
|
|
<div class="handle"
|
|
style="position: absolute; top: calc(2px); right: -2px; width: 4px; height: calc(100% - 8px + 4px); touch-action: none; cursor: e-resize;">
|
|
</div> -->
|
|
</div>
|
|
<div id="try-me"
|
|
style="position: absolute; margin-left: 300px; margin-top: -200px; color: purple; font-weight: bold;">
|
|
<img src="images/about/izquierda.gif" width="49" height="23" alt="hand pointing left"
|
|
style="vertical-align: middle">
|
|
Try me!
|
|
<!-- <span style="color: red">T</span><span style="color: orange">r</span><span style="color: yellow">y</span> <span
|
|
style="color: green">m</span><span style="color: blue">e</span><span style="color: purple">!</span> -->
|
|
</div>
|
|
<p>
|
|
<a href="https://github.com/1j01/jspaint#readme" target="_blank"><img src="images/about/moreinfo_paint.gif"
|
|
width="100" height="60" alt="More info" style="vertical-align: middle"></a>
|
|
Read about the project and extra features on <a href="https://github.com/1j01/jspaint#readme"
|
|
target="_blank">the readme</a>.
|
|
</p>
|
|
<p>
|
|
<a href="https://www.paypal.me/IsaiahOdhner" target="_blank"><img src="images/about/money4.gif" width="32"
|
|
height="32" alt="$" style="vertical-align: middle"></a>
|
|
Support the project at <a href="https://www.paypal.me/IsaiahOdhner"
|
|
target="_blank">paypal.me/IsaiahOdhner</a>.
|
|
</p>
|
|
</section>
|
|
|
|
<section id="windows-98-section">
|
|
<h2>Windows 98 online</h2>
|
|
<p>
|
|
<a href="https://98.js.org" target="_blank">
|
|
<img src="images/about/windows-button.gif" width="40" height="40" alt="Windows logo button">
|
|
<img src="images/about/flagani.gif" style="float: right" width="138" height="251"
|
|
alt="Windows 98 flag pole animation">
|
|
</a>
|
|
JS Paint is also included in a <b>web-based version of Windows 98</b>,
|
|
along with Notepad, Minesweeper, Sound Recorder, Calculator, and Winamp.
|
|
</p>
|
|
<a href="https://98.js.org" target="_blank"
|
|
style="position: relative; display: block; width: 360px; height: 287px">
|
|
<img src="images/about/98.js.org.png" width="360" height="287" alt="windows swung open outwards"
|
|
style="position: absolute; left: 0; top: 0">
|
|
<img src="images/about/WindowCLR_WRK.gif" width="360" height="287" alt="windows swung open outwards"
|
|
id="wooden-window-frame" style="position: absolute; left: 0; top: 0">
|
|
<img src="images/about/ENTERsolar.gif" width="128" height="45" alt="ENTER / 由此進入"
|
|
style="position: absolute; left: 50%; top: 80%; transform: translate(-50%, -50%); mix-blend-mode: lighten">;
|
|
</a>
|
|
</section>
|
|
|
|
<section id="desktop-app-section">
|
|
<h2>Desktop Version</h2>
|
|
<img src="images/about/atombgwht.gif" width="60" height="65" alt="built with Electron" style="float: right;">
|
|
<p>
|
|
There is demand for a desktop version of JS Paint.
|
|
I have put in 99% of the work on this (integration with the file system, wallpaper setting,
|
|
inter-process communication, etc.), but I have not released it yet.
|
|
</p>
|
|
<p>
|
|
If you are motivated, you can
|
|
<a href="https://github.com/1j01/jspaint#desktop-app" target="_blank">manually install</a>
|
|
the desktop app, by cloning the repository and following development setup
|
|
instructions.
|
|
</p>
|
|
<a href="https://github.com/1j01/jspaint/issues/2" target="_blank">
|
|
<img src="images/about/download5.gif" width="100" height="40" alt="Download">
|
|
<img src="images/about/conpaint.gif" width="350" height="50" alt="Under Construction">
|
|
</a>
|
|
</section>
|
|
<section id="pwa-section">
|
|
<h2>Progressive Web App</h2>
|
|
<img src="images/meta/mobipaint.png" width="308" height="193" alt="JS Paint on a phone" style="float: right;">
|
|
<p>
|
|
Alternatively, you can install JS Paint as a PWA (Progressive Web App),
|
|
but this does not <b>yet</b> support offline use
|
|
(as it doesn't include a Service Worker).
|
|
It's more like a bookmark (for now), except it runs in a special window.
|
|
</p>
|
|
<p>
|
|
The user interface for installing PWAs differs by browser and operating system.
|
|
</p>
|
|
<p>
|
|
<q>On most desktop browsers, the install prompt is in the URL bar.
|
|
On mobile, the install prompt is generally found in the menu of browser options.</q>
|
|
See <a
|
|
href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Installing#installing_pwas"
|
|
target="_blank">
|
|
Installing PWAs</a> for visual guidance.
|
|
</p>
|
|
<button id="install-pwa" hidden style="padding: 10px 30px; margin-bottom: 1em;">
|
|
<img src="images/about/monitor.png" width="16" height="16" alt="" style="vertical-align: middle;">
|
|
Install JS Paint
|
|
</button>
|
|
<!-- prevent floated image overlapping next section -->
|
|
<div style="clear: both;"></div>
|
|
</section>
|
|
<section id="textual-paint-section">
|
|
<h2>Textual Paint</h2>
|
|
<p>
|
|
I also made a <i>separate</i> elaborate MS Paint clone that runs in the terminal,
|
|
and edits ANSI art in addition to bitmaps.
|
|
</p>
|
|
<p>
|
|
You can install it with:
|
|
<code>pip install textual-paint</code>
|
|
</p>
|
|
<p>
|
|
And then run with simply:
|
|
<code>textual-paint</code>
|
|
</p>
|
|
<p>
|
|
Requires Python 3.10 or later, and a terminal emulator with Unicode and true color support.
|
|
<ul>
|
|
<li>
|
|
<span class="os-icon"><img src="images/about/windowslogo.gif" width="34" height="30"></span>
|
|
Runs well in <i>Windows Terminal</i>, but not in the older <i>Windows Console</i>.
|
|
</li>
|
|
<li>
|
|
<span class="os-icon"><img src="images/about/maclogo.gif" width="34" height="29"></span>
|
|
Runs well in <i>iTerm2</i>, but not the built in macOS <i>Terminal.app</i>.
|
|
</li>
|
|
<li>
|
|
<span class="os-icon"><img src="images/about/linuxlogo.gif" width="30" height="35"></span>
|
|
Runs well in <i>GNOME Terminal</i>, and most Linux terminal emulators, but not the <i>Linux console</i>.
|
|
</li>
|
|
</ul>
|
|
</p>
|
|
<!-- <img src="images/about/dos-samp.gif" width="391" height="136" alt="MS DOS prompt"> -->
|
|
<img src="images/about/Computer-04-june.gif" width="134" height="133" alt="Computer typing on its own keyboard">
|
|
<p>
|
|
<a href="https://github.com/1j01/textual-paint" target="_blank">Textual Paint</a>
|
|
was built using the <a href="https://textual.textualize.io/" target="_blank">Textual</a>
|
|
framework, which was very fun to work with!
|
|
</p>
|
|
</section>
|
|
|
|
<section id="other-projects-section">
|
|
<h2>Other Projects</h2>
|
|
<p style="float: right;">
|
|
<a href="https://isaiahodhner.io/" target="_blank">
|
|
<img src="images/about/home.gif" width="40" height="40" alt="Home button"
|
|
style="vertical-align: middle">
|
|
Check out my home page for more projects!
|
|
</a>
|
|
</p>
|
|
<p>
|
|
<a href="https://isaiahodhner.io/" target="_blank"
|
|
style="color: rgb(160, 42, 52); font-weight: bold; font-style: italic;">
|
|
<img src="images/about/artcreated.gif" width="165" height="136" alt="Created by">
|
|
<br>
|
|
<span style="display: block; text-align: center; width: 165px;">
|
|
Isaiah Odhner
|
|
</span>
|
|
</a>
|
|
</p>
|
|
<div id="junkbot-area">
|
|
<a href="https://1j01.github.io/janitorial-android/#junkbot" target="_blank">
|
|
<img src="images/about/junkbot-collecting-bin.gif" width="105" height="126"
|
|
alt="LEGO Junkbot minifig eating a recycling bin" id="junkbot-img"></a>
|
|
<span id="feeding-text" hidden>Please don't feed Junkbot.</span>
|
|
</div>
|
|
<a href="https://1j01.github.io/guitar/" target="_blank">
|
|
<img src=" images/about/guitar.gif" width="60" height="100" alt="Guitar" style="float: right;">
|
|
</a>
|
|
<a href="https://1j01.github.io/true-random-movie/" target="_blank">
|
|
<img src="images/about/filmreel.gif" width="116" height="50" alt="film reel" style="float: right;">
|
|
</a>
|
|
<a href="https://1j01.github.io/dat-boi/" target="_blank">
|
|
<img src="images/about/dat-boi-transparent-small.gif" width="96" height="120" alt="Dat Boi"
|
|
style="float: right;">
|
|
</a>
|
|
<a href="https://1j01.github.io/chess-mashup/" target="_blank">
|
|
<img src="images/about/CHESS_CUBE.gif" width="100" height="90" alt="Dat Boi" style="float: right;">
|
|
</a>
|
|
<!-- extends background when floated elements wrap -->
|
|
<div style="clear: both;"></div>
|
|
</section>
|
|
|
|
<script>
|
|
var maxButton = document.querySelector(".window-maximize-button");
|
|
var closeButton = document.querySelector(".window-close-button");
|
|
var minimizeButton = document.querySelector(".window-minimize-button");
|
|
var windowElements = document.querySelectorAll(".window, #try-me");
|
|
|
|
maxButton.addEventListener("click", function () {
|
|
var iframe = document.querySelector("#jspaint-iframe");
|
|
// `iframe.src` doesn't contain the document ID added by the app page.
|
|
// Accessing the iframe's `location.href` allows a more seamless transition,
|
|
// although undos are lost.
|
|
// TODO: Save undo history as part of the session.
|
|
// TODO: Make sure the session is saved before navigating away.
|
|
// if (iframe.contentWindow.undos.length) {
|
|
// if (!confirm("Maximizing will clear your undo history. Continue?")) {
|
|
// return;
|
|
// }
|
|
// }
|
|
location.href = iframe.contentWindow.location.href;
|
|
});
|
|
function hideDemo() {
|
|
for (var i = 0; i < windowElements.length; i++) {
|
|
windowElements[i].style.display = "none";
|
|
}
|
|
}
|
|
closeButton.addEventListener("click", hideDemo);
|
|
minimizeButton.addEventListener("click", hideDemo);
|
|
|
|
// Junkbot "feeding" interaction
|
|
var junkbotArea = document.querySelector("#junkbot-area");
|
|
var junkbotImg = document.querySelector("#junkbot-img");
|
|
var cursorHasBin = true;
|
|
junkbotArea.style.cursor = "url(images/about/junkbot-bin.png) 16 16, auto";
|
|
junkbotImg.src = "images/about/junkbot.gif";
|
|
junkbotImg.addEventListener("mouseenter", function (event) {
|
|
// console.log("mouseenter, cursorHasBin =", cursorHasBin);
|
|
if (!cursorHasBin) {
|
|
return;
|
|
}
|
|
junkbotImg.src = "images/about/junkbot-collecting-bin.gif";
|
|
junkbotArea.style.cursor = "";
|
|
cursorHasBin = false;
|
|
// TODO: make non-looping version of GIF and increase timeout
|
|
// or do this differently, because setTimeout isn't reliable!
|
|
// It's not firing in Firefox sometimes (while loading the page?)
|
|
// and may not fire if the tab is in the background (is switched away from).
|
|
// console.log("setting timeout");
|
|
setTimeout(function () {
|
|
// console.log("timeout");
|
|
junkbotImg.src = "images/about/junkbot.gif";
|
|
junkbotArea.style.cursor = "url(images/about/junkbot-bin.png) 16 16, auto";
|
|
cursorHasBin = true;
|
|
}, 1000);
|
|
});
|
|
document.querySelector("#feeding-text").removeAttribute("hidden");
|
|
</script>
|
|
<!-- split script to keep above ES5-only -->
|
|
<script>
|
|
// PWA install button
|
|
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt
|
|
const installButton = document.querySelector("#install-pwa");
|
|
let installPrompt = null;
|
|
|
|
window.addEventListener("beforeinstallprompt", (event) => {
|
|
event.preventDefault();
|
|
installPrompt = event;
|
|
installButton.removeAttribute("hidden");
|
|
});
|
|
|
|
installButton.addEventListener("click", async () => {
|
|
if (!installPrompt) {
|
|
return;
|
|
}
|
|
const result = await installPrompt.prompt();
|
|
console.log(`Install prompt was: ${result.outcome}`);
|
|
disableInAppInstallPrompt();
|
|
});
|
|
|
|
function disableInAppInstallPrompt() {
|
|
installPrompt = null;
|
|
installButton.setAttribute("hidden", "");
|
|
}
|
|
|
|
window.addEventListener("appinstalled", () => {
|
|
disableInAppInstallPrompt();
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |