Mandala/资源文件/drawmandala.app-main/index.html
2024-10-30 15:19:32 +08:00

1504 lines
36 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Draw a Mandala Online</title>
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/assets/logo/logo-192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/logo/logo-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/logo-16.png">
<link rel="shortcut icon" href="/assets/logo/logo-16.png">
<style type="text/css">
html, body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0px;
background: #AAA;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
#scene {
position: absolute;
z-index: 1;
}
#drawInput {
position: absolute;
top: 0;
left: 0;
z-index: 5;
}
#menu {
position: absolute;
z-index: 10;
width: 5em;
}
#loading-status {
display: none;
position: absolute;
z-index: 10;
top: 30px;
left: 10px;
}
#filePicker, #backgroundPicker {
visibility: hidden;
}
#pallet {
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
width: 30px;
border: 1px solid black;
border-width: 0 0 1px 1px;
}
#colorPicker {
display: block;
box-sizing: border-box;
width: 100%;
}
#pallet .memory {
height: 20px;
}
</style>
<style>
.lds-dual-ring {
display: inline-block;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid rgb(0, 106, 255);
border-color: rgb(0, 106, 255) transparent rgb(0, 106, 255) transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-VM2SHWMG7Q"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-VM2SHWMG7Q');
</script>
</head>
<body>
<select id="menu">
<option value="">Menu</option>
<option value="share">⤴️ Share</option>
<option value="undo">Undo</option>
<option value="save">Save</option>
<option value="load">Load</option>
<option value="clear">💣 Clear</option>
<option value="fill">Fill</option>
<option value="mirroring">Mirroring</option>
<option value="toggleFullscreen">Toggle Fullscreen</option>
<option value="about"> About</option>
</select>
<div id="pallet">
<input type="color" id="colorPicker" />
</div>
<div id="drawInput"></div>
<canvas id="scene"></canvas>
<input type="color" id="backgroundPicker" value="#FFFFFF" />
<input type="file" id="filePicker" />
<div id="loading-status">
<div class="lds-dual-ring"></div>
</div>
<script>
'use strict';
class MyFileStorage {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
initialize() {
return new Promise((resolve, reject) => {
let openReq = window.indexedDB.open(this.dbName, 1);
openReq.onupgradeneeded = ev => {
let db = openReq.result;
db.createObjectStore(this.storeName, {keyPath: 'name'});
};
openReq.onsuccess = ev => {
this.db = openReq.result;
resolve();
};
});
}
async listFiles() {
return await this.queryFiles();
}
async readFile(name) {
let files = await this.queryFiles(name);
switch (files.length) {
case 0:
return null;
case 1:
return files[0];
default:
throw new Error('key has more than one entry');
}
}
writeFile(file) {
return new Promise((resolve, reject) => {
let tx = this.db.transaction(this.storeName, 'readwrite', {durability: 'strict'});
let store = tx.objectStore(this.storeName);
tx.oncomplete = ev => {
resolve();
};
tx.onerror = ev => {
reject(ev.target.error);
};
let putReq = store.put({
name: file.name,
file: file,
});
});
}
queryFiles(query = undefined) {
return new Promise((resolve, reject) => {
let tx = this.db.transaction(this.storeName, 'readonly', {durability: 'strict'});
let store = tx.objectStore(this.storeName);
let cursorReq = store.openCursor(query);
let files = [];
cursorReq.onsuccess = ev => {
let cursor = cursorReq.result;
if (!cursor) {
resolve(files);
} else {
let entry = cursor.value;
files.push(entry.file);
cursor.continue();
}
};
});
}
deleteFile(name) {
return new Promise((resolve, reject) => {
let tx = this.db.transaction(this.storeName, 'readwrite', {durability: 'strict'});
let store = tx.objectStore(this.storeName);
tx.oncomplete = ev => {
resolve();
};
tx.onerror = ev => {
reject(ev.target.error);
};
store.delete(name);
});
}
}
</script>
<script>
'use strict';
const TAU = 2 * Math.PI;
const MAX_SNAPSHOT_HISTORY = 5;
const MAX_COLOR_HISTORY = 25;
const MAX_ZOOM = 2;
const MIN_ZOOM = 0.3;
const appFileStorage = new MyFileStorage('mandala', 'files');
const drawInput = document.getElementById('drawInput');
const menuInput = document.getElementById('menu');
const colorPicker = document.getElementById('colorPicker');
const backgroundPicker = document.getElementById('backgroundPicker');
const filePicker = document.getElementById('filePicker');
const pallet = document.getElementById('pallet');
const loadingStatus = document.getElementById('loading-status');
const primaryCanvas = document.createElement('canvas');
const primaryCtx = primaryCanvas.getContext('2d');
const displayCanvas = document.getElementById('scene');
const displayCtx = displayCanvas.getContext('2d');
let scene = {
w: 3000,
h: 3000,
viewport: {
x: 0,
y: 0,
zoom: 1,
},
drawers: new Map(),
color: 'rgb(0, 0, 0)',
colorHistory: [],
background: 'rgb(255, 255, 255)',
snapshots: [],
sections: 32,
get section() { return TAU / this.sections; }
};
// ---
async function onLoad() {
await appFileStorage.initialize();
await tryMigrateLegacyState();
let state;
try {
state = await restoreState();
} catch (ex) {
alert('Drawing state is corrupted. Resetting.');
await clearState();
state = null;
}
await initialize(state);
}
async function initialize(source) {
resetDisplaySize();
primaryCanvas.width = scene.w;
primaryCanvas.height = scene.h;
if (source) {
loadToPrimary(source);
} else {
clear();
}
centerViewport();
redrawPrimary();
}
async function onResize() {
await sleep(100); // HACK: let screen reflow for a bit
window.scrollTo(0, 0);
resetDisplaySize();
centerViewport();
redrawPrimary();
}
window.addEventListener('load', ev => {
onLoad();
});
window.addEventListener('resize', ev => {
onResize();
});
// ---
const STORAGE_FILE_WORKING = 'WORKING';
function pushSnapshot() {
let snapshot = cloneCanvas(primaryCanvas);
pushCircular(scene.snapshots, snapshot, MAX_SNAPSHOT_HISTORY);
}
async function popSnapshot() {
let snapshot = scene.snapshots.pop();
if (snapshot) {
loadToPrimary(snapshot);
redrawPrimary();
await saveState();
}
}
async function saveState() {
loadingStatus.style.display = 'block';
await setWorkingState(await primaryCanvasToBlob());
loadingStatus.style.display = 'none';
}
async function restoreState() {
let file = await appFileStorage.readFile(STORAGE_FILE_WORKING);
if (file === null) {
return null;
}
return await loadImageAsync(URL.createObjectURL(file));
}
async function clearState() {
await appFileStorage.deleteFile(STORAGE_FILE_WORKING);
}
async function setWorkingState(blob) {
let file = new File([blob], STORAGE_FILE_WORKING);
await appFileStorage.writeFile(file);
}
// ---
const STORAGE_KEY_SCENE = 'scene';
async function legacySaveState() {
let url = cloneCanvas(primaryCanvas).toDataURL();
window.localStorage.setItem(STORAGE_KEY_SCENE, url);
}
async function legacyLoadState() {
let url = window.localStorage.getItem(STORAGE_KEY_SCENE);
if (!url) return null;
return await loadImageAsync(url);
}
async function legacyClearState() {
window.localStorage.removeItem(STORAGE_KEY_SCENE);
}
async function tryMigrateLegacyState() {
let img = await legacyLoadState();
if (img) {
let blob = await canvasToBlob(cloneCanvas(img));
await setWorkingState(blob);
await legacyClearState();
}
}
// ---
async function resetDisplaySize() {
let newW = window.innerWidth;
let newH = window.innerHeight;
displayCanvas.width = newW;
displayCanvas.height = newH;
drawInput.style.width = `${newW}px`;
drawInput.style.height = `${newH}px`;
document.body.style.width = `${newW}px`;
document.body.style.height = `${newH}px`;
}
// ---
function cloneCanvas(source) {
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
newCanvas.width = source.width;
newCanvas.height = source.height;
context.drawImage(source, 0, 0);
return newCanvas;
}
async function openCanvasAsURL() {
let blob = await primaryCanvasToBlob();
let url = window.URL.createObjectURL(blob);
window.open(url, '_blank');
}
async function primaryCanvasToBlob() {
return await canvasToBlob(primaryCanvas);
}
function canvasToBlob(canvas) {
return new Promise(resolve => {
canvas.toBlob(blob => {
resolve(blob);
});
});
}
// ---
function clear() {
primaryCtx.clearRect(0, 0, primaryCanvas.width, primaryCanvas.height);
displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height);
fillCanvas(primaryCtx, scene.background);
fillCanvas(displayCtx, scene.background);
}
function fillCanvas(ctx, color) {
ctx.save();
ctx.fillStyle = color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.restore();
}
function loadToPrimary(source) {
primaryCtx.drawImage(source, 0, 0, source.width, source.height, 0, 0, scene.w, scene.h);
}
function redrawPrimary() {
let dx = 0;
let dy = 0;
let dw = displayCanvas.width;
let dh = displayCanvas.height;
let sx = scene.viewport.x;
let sy = scene.viewport.y;
let sw = dw / scene.viewport.zoom;
let sh = dh / scene.viewport.zoom;
displayCtx.clearRect(0, 0, dw, dh);
displayCtx.drawImage(...getSafeRect(primaryCanvas, sx, sy, sw, sh, dx, dy, dw, dh))
}
// ---
function viewportToScene(x, y) {
let sceneX = scene.viewport.x + x / scene.viewport.zoom;
let sceneY = scene.viewport.y + y / scene.viewport.zoom;
return [sceneX, sceneY];
}
function sceneToViewport(x, y) {
let vx = (x - scene.viewport.x) * scene.viewport.zoom;
let vy = (y - scene.viewport.y) * scene.viewport.zoom;
return [vx, vy];
}
function viewportDimensions() {
let vw = displayCanvas.width;
let vh = displayCanvas.height;
return [vw, vh];
}
function centerViewport() {
let [vw, vh] = viewportDimensions();
let wz = vw / scene.w;
let hz = vh / scene.h;
let z = Math.max(wz, hz);
let dx = (scene.w - vw / z) / 2;
let dy = (scene.h - vh / z) / 2;
scene.viewport.x = dx;
scene.viewport.y = dy;
scene.viewport.zoom = z;
}
function panViewportBy(dx, dy) {
scene.viewport.x += dx / scene.viewport.zoom;
scene.viewport.y += dy / scene.viewport.zoom;
}
// this was hard...
function zoomViewportAt(x, y, dz) {
let z = scene.viewport.zoom;
let z_ = z + dz;
if ((z_ < MIN_ZOOM && dz < 0) || (z > MAX_ZOOM && dz > 0)) return;
let [vw, vh] = viewportDimensions();
let rx = x / vw;
let ry = y / vh;
let svw = vw / scene.viewport.zoom;
let svh = vh / scene.viewport.zoom;
let rz = z / z_;
let dsvw = svw * rz - svw;
let dsvh = svh * rz - svh;
let dvx = -rx * dsvw;
let dvy = -ry * dsvh;
scene.viewport.x += dvx;
scene.viewport.y += dvy;
scene.viewport.zoom += dz;
}
// ---
class Drawer {
constructor(color) {
this.points = [];
this.color = color || 'rgb(0, 0, 0)';
this.tool = new Croquis.Brush();
this.tool.setDrawFunction(drawMandalaPoint);
this.tool.setSize(10);
}
down(item) {
this.stabilizer = new Croquis.Stabilizer(
this.tool.down,
this.tool.move,
this.tool.up,
10,
0.5,
item.x,
item.y,
item.pressure
);
}
move(item) {
this.stabilizer.move(
item.x,
item.y,
item.pressure
);
}
up(item) {
this.stabilizer.up(
item.x,
item.y,
item.pressure
);
}
}
function drawMandalaPoint(x, y, size) {
let [sceneX, sceneY] = viewportToScene(x, y);
let [cx, cy] = sceneToViewport(scene.w/2, scene.h/2);
drawMandalaPointDirect(primaryCtx, scene.w/2, scene.h/2, sceneX, sceneY, size);
drawMandalaPointDirect(displayCtx, cx, cy, x, y, size * scene.viewport.zoom);
}
function drawMandalaPointDirect(ctx, cx, cy, x, y, size) {
let w = ctx.canvas.width;
let h = ctx.canvas.height;
let p = new Vec2(
x - cx,
cy - y
);
var halfSize = size * 0.5;
ctx.save();
ctx.translate(cx, cy);
for (let i = 0; i < scene.sections; ++i) {
let b = scene.section * i - p.angle() * Math.pow(-1, i);
let q = Vec2.fromRad(b).scaled(p.length());
ctx.save();
ctx.fillStyle = scene.color;
ctx.beginPath();
ctx.arc(q.x, q.y, halfSize, 0, TAU);
ctx.closePath();
ctx.fill();
ctx.restore();
}
ctx.restore();
}
// ---
function getPointerPressure(ev) {
switch (ev.pointerType) {
case 'mouse':
case 'touch':
return 0.5;
case 'pen':
return ev.pressure;
default:
return 0.5;
}
}
function readPointerEvent(ev) {
let rect = drawInput.getBoundingClientRect();
return {
x: ev.clientX - rect.left,
y: ev.clientY - rect.top,
pressure: getPointerPressure(ev)
};
}
function handleDrawDown(ev) {
pushSnapshot();
let drawer = new Drawer(colorPicker.value);
drawer.down(readPointerEvent(ev));
scene.drawers.set(ev.pointerId, drawer);
}
function handleDrawMove(ev) {
let drawer = scene.drawers.get(ev.pointerId);
if (drawer) {
let events = 'getCoalescedEvents' in ev ? ev.getCoalescedEvents() : [ev];
for (let e of events) {
drawer.move(readPointerEvent(e));
}
}
}
function handleDrawUp(ev) {
let drawer = scene.drawers.get(ev.pointerId);
if (drawer) {
drawer.up(readPointerEvent(ev));
scene.drawers.delete(ev.pointerId);
sleep(100).then(() => saveState());
}
}
(() => {
const STATE_DRAWING = 'DRAWING';
const GESTURE_TIMEOUT = 50;
let pointerDownEvents = new Map();
let state = '';
let drawTimeout = 0;
let startZoom = 0;
let startDistance = 0;
drawInput.addEventListener('pointerdown', ev => {
ev.preventDefault();
ev.stopPropagation();
pointerDownEvents.set(ev.pointerId, ev);
switch (pointerDownEvents.size) {
case 1:
drawTimeout = setTimeout(() => {
state = STATE_DRAWING;
handleDrawDown(ev);
}, GESTURE_TIMEOUT);
break;
case 2:
if (state === STATE_DRAWING) {
pointerDownEvents.delete(ev.pointerId, ev);
return;
}
clearTimeout(drawTimeout);
startZoom = scene.viewport.zoom;
startDistance = pointerPairDistance(...pointerDownEvents.values());
break;
}
});
function pointerPairCenter(pa, pb) {
// TODO relative to target rect?
let cx = (pa.clientX + pb.clientX) / 2;
let cy = (pa.clientY + pb.clientY) / 2;
return [cx, cy];
}
function pointerPairDistance(pa, pb) {
// TODO relative to target rect?
let dx = pa.clientX - pb.clientX;
let dy = pa.clientY - pb.clientY;
return Math.sqrt(dx*dx + dy*dy);
}
drawInput.addEventListener('pointermove', ev => {
ev.preventDefault();
ev.stopPropagation();
switch (pointerDownEvents.size) {
case 1:
handleDrawMove(ev);
break;
case 2:
let [ax, ay] = pointerPairCenter(...pointerDownEvents.values());
pointerDownEvents.set(ev.pointerId, ev);
let [bx, by] = pointerPairCenter(...pointerDownEvents.values());
let bs = pointerPairDistance(...pointerDownEvents.values());
let dx = bx - ax;
let dy = by - ay;
let ds = bs / startDistance;
panViewportBy(-dx, -dy);
let newZoom = startZoom * ds;
let dz = newZoom - scene.viewport.zoom;
zoomViewportAt(bx, by, dz);
redrawPrimary();
break;
}
});
function handlePointerUp(ev) {
ev.preventDefault();
ev.stopPropagation();
handleDrawUp(ev);
pointerDownEvents.delete(ev.pointerId);
switch (pointerDownEvents.size) {
case 0:
state = '';
break;
}
}
drawInput.addEventListener('pointerup', ev => {
handlePointerUp(ev);
});
drawInput.addEventListener('pointerout', ev => {
handlePointerUp(ev);
});
drawInput.addEventListener('pointercancel', ev => {
handlePointerUp(ev);
});
drawInput.addEventListener('wheel', ev => {
ev.preventDefault();
if (state !== STATE_DRAWING) {
if (ev.ctrlKey) {
//scene.viewport.zoom += ev.deltaY * 0.01;
zoomViewportAt(ev.clientX, ev.clientY, -ev.deltaY * 0.01);
} else {
panViewportBy(ev.deltaX * 2, ev.deltaY * 2);
}
redrawPrimary();
}
});
window.addEventListener("gesturestart", function (e) {
e.preventDefault();
startZoom = scene.viewport.zoom;
});
window.addEventListener("gesturechange", function (e) {
e.preventDefault();
if (state !== STATE_DRAWING) {
let newZoom = startZoom * e.scale;
zoomViewportAt(e.clientX, e.clientY, newZoom - scene.viewport.zoom);
redrawPrimary();
}
})
window.addEventListener("gestureend", function (e) {
e.preventDefault();
});
drawInput.oncontextmenu = ev => {
ev.preventDefault();
};
})();
// ---
function canOpenFile() {
return !!window.showOpenFilePicker;
}
async function openFileToCanvas() {
let [fileHandle] = await window.showOpenFilePicker({
startIn: 'pictures',
multiple: false,
excludeAcceptAllOption: true,
types: [{
description: 'Images',
accept: {
'image/bmp': ['.bmp'],
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/webp': ['.webp']
},
}],
});
let file = await fileHandle.getFile();
let url = await readFileAsDataURLAsync(file);
let img = await loadImageAsync(url);
pushSnapshot();
await initialize(img);
await saveState();
}
function canSaveFile() {
return !!window.showSaveFilePicker;
}
async function saveCanvasAsFile() {
let fileHandle = await window.showSaveFilePicker({
id: 'importImage',
suggestedName: 'Mandala.png',
types: [{
description: 'Images',
accept: {
'image/png': ['.png'],
},
}],
});
let writable = await fileHandle.createWritable();
await writable.write(await primaryCanvasToBlob());
await writable.close();
}
// ---
const menuActions = {
toggleFullscreen() {
if (currentFullscreen()) {
exitFullscreen();
} else {
openFullscreen(document.body);
}
},
async save() {
if (canSaveFile()) {
await saveCanvasAsFile();
} else {
await openCanvasAsURL();
}
},
async load() {
if (canOpenFile()) {
await openFileToCanvas();
} else {
filePicker.click();
}
},
async clear() {
clear();
await saveState();
},
fill() {
backgroundPicker.value = '#000001';
backgroundPicker.click();
},
async undo() {
await popSnapshot();
},
mirroring() {
let input = window.prompt('Pick mirroring (even number)', scene.sections);
let value = parseInt(input, 10);
if (isNaN(value) || value <= 0 || value % 2 != 0) {
alert('Try again with an even number.');
return;
}
scene.sections = value;
},
about() {
window.open('https://github.com/Garciat/drawmandala.app', '_blank');
},
async share() {
let blob = await primaryCanvasToBlob();
let file = new File([blob], "mandala.png", {type: 'image/png'});
let files = [file];
let shareData = {
files: files,
};
if (navigator.share) {
if (navigator.canShare && !navigator.canShare({files: files})) {
alert(`Your system cannot share this file.`);
return;
}
await navigator.share(shareData);
} else {
alert(`Your system doesn't support sharing files.`);
}
}
};
menuInput.addEventListener('change', ev => {
menuActions[menuInput.value]();
menuInput.value = '';
});
backgroundPicker.addEventListener('change', ev => {
scene.background = backgroundPicker.value;
clear();
saveState();
});
filePicker.addEventListener('change', async function (ev) {
let files = ev.target.files;
let file = files[0];
if (!file.type.match('image.*')) {
alert('Only image files, please.');
return;
}
filePicker.value = null;
let url = await readFileAsDataURLAsync(file);
let img = await loadImageAsync(url);
pushSnapshot();
await initialize(img);
await saveState();
});
function readFileAsDataURLAsync(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
});
}
function loadImageAsync(url) {
return new Promise((resolve, reject) => {
let img = new window.Image();
img.onload = () => resolve(img);
img.onerror = ev => reject(new Error('Could not load image from URL: ' + url));
img.src = url;
});
}
// ---
function setColor(value) {
addColorHistory(scene.color);
colorPicker.value = value;
scene.color = value;
}
function addColorHistory(value) {
if (isInColorHistory(value)) return;
unshiftCircular(scene.colorHistory, value, MAX_COLOR_HISTORY);
renderColorHistory();
}
function isInColorHistory(value) {
return scene.colorHistory.indexOf(value) >= 0;
}
function renderColorHistory() {
for (let memory of document.querySelectorAll('#pallet .memory')) {
memory.parentNode.removeChild(memory);
}
for (let value of scene.colorHistory) {
let memory = document.createElement('div');
memory.dataset.value = value;
memory.classList.add('memory');
memory.style.background = value;
pallet.appendChild(memory);
}
}
colorPicker.addEventListener('change', ev => {
setColor(colorPicker.value);
});
pallet.addEventListener('click', ev => {
if (ev.target.classList.contains('memory')) {
setColor(ev.target.dataset.value);
}
});
window.addEventListener('load', ev => {
addColorHistory(rgbToHex(248,244,238));
addColorHistory(rgbToHex(255,228,225));
addColorHistory(rgbToHex(246,84,106));
addColorHistory(rgbToHex(0,206,209));
addColorHistory(rgbToHex(26,28,114));
addColorHistory(rgbToHex(255, 255, 255));
});
// ---
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
function getPWADisplayMode() {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
if (document.referrer.startsWith('android-app://')) {
return 'twa';
} else if (navigator.standalone || isStandalone) {
return 'standalone';
}
return 'browser';
}
window.addEventListener('beforeinstallprompt', (event) => {
// nothing
});
window.addEventListener('appinstalled', (event) => {
gtag('event', 'pwa_install');
});
// ---
function rgbToHex(r, g, b) {
return `#${octetHex(r)}${octetHex(g)}${octetHex(b)}`;
}
function octetHex(x) {
return x.toString(16).padStart(2, '0');
}
// ---
class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
atan2() {
return Math.atan2(this.y, this.x);
}
angle() {
let a = this.atan2();
return a >= 0 ? a : (TAU + a);
}
lengthSq() {
return this.x*this.x + this.y*this.y;
}
length() {
return Math.sqrt(this.lengthSq());
}
scaled(k) {
return new Vec2(k * this.x, k * this.y);
}
static fromRad(r) {
return new Vec2(Math.cos(r), Math.sin(r));
}
}
Vec2.X = new Vec2(1, 0);
Vec2.Y = new Vec2(0, 1);
// ---
function openFullscreen(elem) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
function currentFullscreen() {
return (
document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullScreenElement
);
}
// ---
function lastElement(xs) {
return xs[xs.length - 1];
}
function pushCircular(xs, x, max) {
xs.push(x);
while (xs.length > max) xs.shift();
}
function unshiftCircular(xs, x, max) {
xs.unshift(x);
while (xs.length > max) xs.pop();
}
// ---
function sleep(t) {
return new Promise(resolve => {
setTimeout(() => resolve(), t);
});
}
// ---
// https://gist.github.com/Kaiido/ca9c837382d89b9d0061e96181d1d862
function getSafeRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
const { width, height } = source;
if( sw < 0 ) {
sx += sw;
sw = Math.abs( sw );
}
if( sh < 0 ) {
sy += sh;
sh = Math.abs( sh );
}
if( dw < 0 ) {
dx += dw;
dw = Math.abs( dw );
}
if( dh < 0 ) {
dy += dh;
dh = Math.abs( dh );
}
const x1 = Math.max( sx, 0 );
const x2 = Math.min( sx + sw, width );
const y1 = Math.max( sy, 0 );
const y2 = Math.min( sy + sh, height );
const w_ratio = dw / sw;
const h_ratio = dh / sh;
return [
source,
x1,
y1,
x2 - x1,
y2 - y1,
sx < 0 ? dx - (sx * w_ratio) : dx,
sy < 0 ? dy - (sy * h_ratio) : dy,
(x2 - x1) * w_ratio,
(y2 - y1) * h_ratio
];
}
// ---
const Croquis = {};
Croquis.Stabilizer = function (down, move, up, level, weight,
x, y, pressure, interval) {
interval = interval || 5;
var follow = 1 - Math.min(0.95, Math.max(0, weight));
var paramTable = [];
var current = { x: x, y: y, pressure: pressure };
for (var i = 0; i < level; ++i)
paramTable.push({ x: x, y: y, pressure: pressure });
var first = paramTable[0];
var last = paramTable[paramTable.length - 1];
var upCalled = false;
if (down != null)
down(x, y, pressure);
window.setTimeout(_move, interval);
this.getParamTable = function () { //for test
return paramTable;
}
this.move = function (x, y, pressure) {
current.x = x;
current.y = y;
current.pressure = pressure;
}
this.up = function (x, y, pressure) {
current.x = x;
current.y = y;
current.pressure = pressure;
upCalled = true;
}
function dlerp(a, d, t) {
return a + d * t;
}
function _move(justCalc) {
var curr;
var prev;
var dx;
var dy;
var dp;
var delta = 0;
first.x = current.x;
first.y = current.y;
first.pressure = current.pressure;
for (var i = 1; i < paramTable.length; ++i) {
curr = paramTable[i];
prev = paramTable[i - 1];
dx = prev.x - curr.x;
dy = prev.y - curr.y;
dp = prev.pressure - curr.pressure;
delta += Math.abs(dx);
delta += Math.abs(dy);
curr.x = dlerp(curr.x, dx, follow);
curr.y = dlerp(curr.y, dy, follow);
curr.pressure = dlerp(curr.pressure, dp, follow);
}
if (justCalc)
return delta;
if (upCalled) {
while(delta > 1) {
move(last.x, last.y, last.pressure);
delta = _move(true);
}
up(last.x, last.y, last.pressure);
}
else {
move(last.x, last.y, last.pressure);
window.setTimeout(_move, interval);
}
}
};
Croquis.Brush = function () {
// math shortcut
var min = Math.min;
var max = Math.max;
var abs = Math.abs;
var sin = Math.sin;
var cos = Math.cos;
var sqrt = Math.sqrt;
var atan2 = Math.atan2;
var PI = Math.PI;
var ONE = PI + PI;
var QUARTER = PI * 0.5;
var random = Math.random;
this.setRandomFunction = function (value) {
random = value;
}
var color = '#000';
this.getColor = function () {
return color;
}
this.setColor = function (value) {
color = value;
}
var flow = 1;
this.getFlow = function() {
return flow;
}
this.setFlow = function(value) {
flow = value;
}
var size = 10;
this.getSize = function () {
return size;
}
this.setSize = function (value) {
size = (value < 1) ? 1 : value;
}
var spacing = 0.2;
this.getSpacing = function () {
return spacing;
}
this.setSpacing = function (value) {
spacing = (value < 0.01) ? 0.01 : value;
}
var toRad = PI / 180;
var toDeg = 1 / toRad;
var angle = 0; // radian unit
this.getAngle = function () { // returns degree unit
return angle * toDeg;
}
this.setAngle = function (value) {
angle = value * toRad;
}
var rotateToDirection = false;
this.getRotateToDirection = function () {
return rotateToDirection;
}
this.setRotateToDirection = function (value) {
rotateToDirection = value;
}
var normalSpread = 0;
this.getNormalSpread = function () {
return normalSpread;
}
this.setNormalSpread = function (value) {
normalSpread = value;
}
var tangentSpread = 0;
this.getTangentSpread = function () {
return tangentSpread;
}
this.setTangentSpread = function (value) {
tangentSpread = value;
}
var delta = 0;
var prevX = 0;
var prevY = 0;
var lastX = 0;
var lastY = 0;
var dir = 0;
var prevScale = 0;
var drawFunction = undefined;
this.setDrawFunction = function (f) {
drawFunction = f;
}
var reserved = null;
var dirtyRect;
function spreadRandom() {
return random() - 0.5;
}
function drawReserved() {
if (reserved != null) {
drawTo(reserved.x, reserved.y, reserved.scale);
reserved = null;
}
}
function appendDirtyRect(x, y, width, height) {
if (!(width && height))
return;
var dxw = dirtyRect.x + dirtyRect.width;
var dyh = dirtyRect.y + dirtyRect.height;
var xw = x + width;
var yh = y + height;
var minX = dirtyRect.width ? min(dirtyRect.x, x) : x;
var minY = dirtyRect.height ? min(dirtyRect.y, y) : y;
dirtyRect.x = minX;
dirtyRect.y = minY;
dirtyRect.width = max(dxw, xw) - minX;
dirtyRect.height = max(dyh, yh) - minY;
}
function drawTo(x, y, scale) {
var scaledSize = size * scale;
var nrm = dir + QUARTER;
var nr = normalSpread * scaledSize * spreadRandom();
var tr = tangentSpread * scaledSize * spreadRandom();
var ra = rotateToDirection ? angle + dir : angle;
var width = scaledSize;
var height = width;
var boundWidth = abs(height * sin(ra)) + abs(width * cos(ra));
var boundHeight = abs(width * sin(ra)) + abs(height * cos(ra));
x += Math.cos(nrm) * nr + Math.cos(dir) * tr;
y += Math.sin(nrm) * nr + Math.sin(dir) * tr;
drawFunction(x, y, width);
appendDirtyRect(x - (boundWidth * 0.5),
y - (boundHeight * 0.5),
boundWidth, boundHeight);
}
this.down = function(x, y, scale) {
dir = 0;
dirtyRect = {x: 0, y: 0, width: 0, height: 0};
if (scale > 0) {
if (rotateToDirection || normalSpread != 0 || tangentSpread != 0)
reserved = {x: x, y: y, scale: scale};
else
drawTo(x, y, scale);
}
delta = 0;
lastX = prevX = x;
lastY = prevY = y;
prevScale = scale;
}
this.move = function(x, y, scale) {
if (scale <= 0) {
delta = 0;
prevX = x;
prevY = y;
prevScale = scale;
return;
}
var dx = x - prevX;
var dy = y - prevY;
var ds = scale - prevScale;
var d = sqrt(dx * dx + dy * dy);
prevX = x;
prevY = y;
delta += d;
var midScale = (prevScale + scale) * 0.5;
var drawSpacing = size * spacing * midScale;
var ldx = x - lastX;
var ldy = y - lastY;
var ld = sqrt(ldx * ldx + ldy * ldy);
dir = atan2(ldy, ldx);
if (ldx || ldy)
drawReserved();
if (drawSpacing < 0.5)
drawSpacing = 0.5;
if (delta < drawSpacing) {
prevScale = scale;
return;
}
var scaleSpacing = ds * (drawSpacing / delta);
if (ld < drawSpacing) {
lastX = x;
lastY = y;
drawTo(lastX, lastY, scale);
delta -= drawSpacing;
} else {
while(delta >= drawSpacing) {
ldx = x - lastX;
ldy = y - lastY;
var tx = cos(dir);
var ty = sin(dir);
lastX += tx * drawSpacing;
lastY += ty * drawSpacing;
prevScale += scaleSpacing;
drawTo(lastX, lastY, prevScale);
delta -= drawSpacing;
}
}
prevScale = scale;
}
this.up = function (x, y, scale) {
dir = atan2(y - lastY, x - lastX);
drawReserved();
return dirtyRect;
}
};
</script>
</body>
</html>