mirror of
https://github.com/vgstation-coders/vgstation13.git
synced 2025-12-10 10:21:11 +00:00
Custom painting revival (#31563)
* First steps. Template and some BYOND-side work * Change of plans, fuck NanoUI, we html_interface nao. Canvas painting code moved to it's own .dm, .tmpl split into .tpml and .js. You can use a pen on a canvas to start drawing, can't save your drawing yet but it loads whatever data was already in there. So far so good. * Tweaks nanotrasen.css cause it SUCKS. Link buttons behave like shared.css', and text inputs look better (that border had no business being so thick). TODO: Check the MSGS' _UI cause it had a button input that looked like ass because of the CSS applying to all inputs * UI and UI code overhaul. Topic receives data. TODO: clean up UI code, It's a mess. paintTool.js should probably get split up further, separate the painting from the blending modes or something. * Sanity, saving, description, and href-multi integration * Yay, procastrination. Also, split UI and Topic logic into reusable datum separate from object, and some more sanity. I really should get to the "display the painting in-game" part at some point * IT WORKS! Paintings are showing up in game. * Crayons! And pens and color and stuff * Soap to clean/blank, and moving "Title" before "Author" on UI * Couple fixes * Paint brushes! Icons may need replacing, specially inhands * Cyan paint bucket. UPDATE WIKI: SUPPLY PACKS * Container fix. Picking a human's blood color as paint is funny, but weird * Can't quite remember what I was up to last time, moved files to their own folder on /module I guess. Just, getting my stuff out there for a PR * Supply crate and Eneocho's glass idea * fugg * Color transparency and movement throttling * Hair dye spray cans. Outperformed by rainbow crayons, those get two colors on top of the picker. Should implement brush size to give them an actual advantage. Maybe stronger paint? * Fixes a couple oopsies after rebasing to (as of writing) current Bleeding Edge * new icons * .dme * Palette Co-authored-by: JellyVeggie2 <78439377+jellyveggie2@users.noreply.github.com>
This commit is contained in:
@@ -51,6 +51,9 @@
|
||||
parent = null
|
||||
parts = null
|
||||
|
||||
/datum/href_multipart_handler/proc/set_parent(parent)
|
||||
src.parent = parent
|
||||
|
||||
/datum/href_multipart_handler/Topic(href, href_list)
|
||||
if(href_list["multipart"])
|
||||
// Initialize the list to the size specified by 'multipart-total'
|
||||
|
||||
@@ -46,6 +46,10 @@ hr
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.link, .linkOn, .linkOff, .selected, .disabled, .button {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
a, a:link, a:visited, a:active, .linkOn, .linkOff
|
||||
{
|
||||
color: #ffffff;
|
||||
@@ -73,10 +77,10 @@ a.nobg:hover
|
||||
color:#40628a;
|
||||
}
|
||||
|
||||
a:hover
|
||||
{
|
||||
color: #40628a;
|
||||
background: #ffffff;
|
||||
a:hover, .zoomLink:hover, .linkActive:hover, .buttonActive:hover {
|
||||
color:#ffffff;
|
||||
background: #507aac;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.white, a.white:link, a.white:visited, a.white:active
|
||||
@@ -357,10 +361,9 @@ div.notice
|
||||
clear: both;
|
||||
}
|
||||
|
||||
input
|
||||
input[type="text"], textarea
|
||||
{
|
||||
background-color: #202020;
|
||||
border-color: #40628a;
|
||||
border-style: solid;
|
||||
border: 1px solid #40628a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
99
code/modules/html_interface/paintTool/canvas.css
Normal file
99
code/modules/html_interface/paintTool/canvas.css
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
.mainPanel {
|
||||
height:650px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mainPanel > * {
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.controlColumn {
|
||||
overflow: auto;
|
||||
height:100%;
|
||||
width: 250px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.controlColumn input[type="text"] {
|
||||
width:100%;
|
||||
}
|
||||
.controlColumn textarea {
|
||||
width:100%;
|
||||
max-width:100%;
|
||||
min-width:100%;
|
||||
}
|
||||
|
||||
.colorHint {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.navigationBar a {
|
||||
width: 32.3%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.paintColumn {
|
||||
min-width: 320px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.canvasPanel {
|
||||
border: 1px solid #40628a;
|
||||
margin: 19px;
|
||||
background-color: #202020;
|
||||
line-height: 0px;
|
||||
}
|
||||
|
||||
.canvasPanel p {
|
||||
line-height: normal;
|
||||
position: absolute;
|
||||
width: 130px;
|
||||
margin: 10px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.canvasPanel canvas {
|
||||
background: #202020;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.paintPanel {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.paintPanel label {
|
||||
margin-right:20px;
|
||||
}
|
||||
.colorPanel {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selectedColor {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.palette {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.selectedColor div {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.palette div {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-block;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.toolPanel input[type="text"] {
|
||||
width: 5ch;
|
||||
text-align:center
|
||||
}
|
||||
300
code/modules/html_interface/paintTool/canvas.js
Normal file
300
code/modules/html_interface/paintTool/canvas.js
Normal file
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* ### canvas.js ###
|
||||
* Scripts meant to handle canvas.html
|
||||
*/
|
||||
|
||||
// Tab selector
|
||||
var panelIdList = ["infoPanel", "templatePanel", "exportPanel"];
|
||||
function panelSelect(panelId) {
|
||||
var panelClass;
|
||||
var buttonClass;
|
||||
for (id in panelIdList) {
|
||||
id = panelIdList[id];
|
||||
panelClass = document.getElementById(id).classList;
|
||||
buttonClass = document.getElementById(id + "Button").classList;
|
||||
if (id != panelId) {
|
||||
panelClass.add("hidden");
|
||||
buttonClass.remove("linkOn");
|
||||
} else {
|
||||
panelClass.remove("hidden");
|
||||
buttonClass.add("linkOn");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleClass(id, className) {
|
||||
var classList = document.getElementById(id).classList;
|
||||
if (classList.contains(className)) {
|
||||
classList.remove(className);
|
||||
} else {
|
||||
classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHelp(helpButton, helpPanel) {
|
||||
toggleClass(helpButton, "linkOn");
|
||||
toggleClass(helpPanel, "hidden");
|
||||
}
|
||||
|
||||
// Template control
|
||||
var currentTemplate = {};
|
||||
/* A test template for 14x14 sized paintings
|
||||
{
|
||||
"rgn":[
|
||||
{"clr":"#ffffff","txt":"Mask"},
|
||||
{"clr":"#ffdddd","txt":"Mask border"},
|
||||
{"clr":"#ff8800","txt":"Hair"},
|
||||
{"clr":"#ee6000","txt":"Hair shade"},
|
||||
{"clr":"#ffff00","txt":"Eyeliner"},
|
||||
{"clr":"#0000ff","txt":"Eyes"},
|
||||
{"clr":"#ff8888","txt":"Nose shade"},
|
||||
{"clr":"#ff0000","txt":"Nose"},
|
||||
{"clr":"#000000","txt":"Background"}
|
||||
],
|
||||
"bmp": [
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
2,2,2,8,8,8,8,8,8,8,2,2,2,8,
|
||||
2,2,2,2,2,8,8,8,2,2,2,2,2,2,
|
||||
2,2,2,2,1,1,1,1,1,2,2,2,2,2,
|
||||
2,2,3,3,1,4,0,4,1,3,3,2,2,2,
|
||||
3,3,3,1,1,5,4,5,1,1,3,3,3,2,
|
||||
3,3,3,1,0,4,0,4,0,1,3,3,3,8,
|
||||
8,8,8,1,0,6,7,6,0,1,8,8,8,8,
|
||||
8,8,8,1,1,0,6,0,1,1,8,8,8,8,
|
||||
8,8,8,8,1,1,1,1,1,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
function templateMover(id, isTargetToDone) {
|
||||
var item = document.getElementById("tp-" + id);
|
||||
|
||||
// "Disable" item's buttons (show disabled versions, hide enabled versions)
|
||||
var items = item.getElementsByClassName("tp-pending-class");
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (isTargetToDone)
|
||||
items[i].classList.add("hidden");
|
||||
else
|
||||
items[i].classList.remove("hidden");
|
||||
}
|
||||
|
||||
items = item.getElementsByClassName("tp-done-class");
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (isTargetToDone)
|
||||
items[i].classList.remove("hidden");
|
||||
else
|
||||
items[i].classList.add("hidden");
|
||||
}
|
||||
|
||||
// Move item to Done list
|
||||
var target = isTargetToDone ? "doneRegions" : "pendingRegions";
|
||||
item.parentElement.removeChild(item);
|
||||
items = document.getElementById(target).children;
|
||||
inserted = false;
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (items[i].id > ("tp-" + id)) {
|
||||
items[i].insertAdjacentElement("beforebegin", item);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) document.getElementById(target).insertAdjacentElement("beforeend", item);
|
||||
}
|
||||
|
||||
|
||||
function templatePending(id) {
|
||||
templateMover(id, false);
|
||||
}
|
||||
|
||||
function templateDone(id) {
|
||||
templateMover(id, true);
|
||||
}
|
||||
|
||||
var colorRegex = /^#[0-9A-Fa-f]{6}$/;
|
||||
function buildTemplateItem(id, colorHint, textHint) {
|
||||
// Create a new color region element
|
||||
var item = document.createElement("LI");
|
||||
item.id = "tp-" + id;
|
||||
item.classList.add("item");
|
||||
|
||||
// Color hint. Invalid colors get a red cross over a white background instead
|
||||
var missingColorIcon = "";
|
||||
if (!colorRegex.test(colorHint)) {
|
||||
missingColorIcon = '<span style="color: #ff0000; font-weight: bold">X</span>';
|
||||
colorHint = "#ffffff";
|
||||
}
|
||||
item.innerHTML = '<a class="button colorHint" style="background: {color}">{icon}</a>'
|
||||
.replace("{color}", colorHint).replace("{icon}", missingColorIcon);
|
||||
|
||||
// Paint button, and disabled button variant
|
||||
item.innerHTML +=
|
||||
'<a class="tp-pending-class button" onclick="templatePaint(\'{id}\');">Paint Over</a>'
|
||||
.replace("{id}", id)
|
||||
+ '<a class="tp-done-class button linkOff hidden">Paint Over</a>';
|
||||
|
||||
// Text hint toggle button
|
||||
item.innerHTML +=
|
||||
'<a class="button" id="tp-{id}-infoButton" onclick="toggleHelp(\'tp-{id}-infoButton\', \'tp-{id}-help\');"><div class="uiIcon16 icon-info"></div></a>'
|
||||
.replace("{id}", id).replace("{id}", id).replace("{id}", id);
|
||||
|
||||
// Done/Pending button
|
||||
item.innerHTML +=
|
||||
'<a class="tp-pending-class button" onclick="templateDone(\'{id}\')"><div class="uiIcon16 icon-carat-1-s"></div></a>'
|
||||
.replace("{id}", id)
|
||||
+ '<a class="tp-done-class button hidden" onclick="templatePending(\'{id}\')"><div class="uiIcon16 icon-carat-1-n"></div></a>'
|
||||
.replace("{id}", id);
|
||||
|
||||
// Text hint. Actual text added through innerText, lest players inject their own html
|
||||
var helpText = document.createElement("P");
|
||||
helpText.innerText = textHint;
|
||||
item.innerHTML +=
|
||||
'<div id="tp-{id}-help" class="line hidden"></div>'
|
||||
.replace("{id}", id);
|
||||
item.lastChild.appendChild(helpText);
|
||||
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
function loadTemplate(template) {
|
||||
//Empty both Pending and Done lists
|
||||
var itemLister = document.getElementById("doneRegions");
|
||||
while (itemLister.childElementCount > 0) {
|
||||
itemLister.removeChild(itemLister.firstChild);
|
||||
}
|
||||
itemLister = document.getElementById("pendingRegions");
|
||||
while (itemLister.childElementCount > 0) {
|
||||
itemLister.removeChild(itemLister.firstChild);
|
||||
}
|
||||
|
||||
// Fill Pending list with new color regions
|
||||
currentTemplate = JSON.parse(template);
|
||||
for (i = 0; i < currentTemplate.rgn.length; i++) {
|
||||
itemLister.appendChild(buildTemplateItem(i, currentTemplate.rgn[i].clr, currentTemplate.rgn[i].txt));
|
||||
}
|
||||
}
|
||||
|
||||
function exportTemplate() {
|
||||
var output = document.getElementById("export-text");
|
||||
var template = {rgn: [], bmp: []};
|
||||
|
||||
var colors = [];
|
||||
for (pixel in bitmap) {
|
||||
if (colors.indexOf(bitmap[pixel]) < 0) {
|
||||
colors.push(bitmap[pixel]);
|
||||
template.rgn.push({clr: bitmap[pixel], txt: ""});
|
||||
}
|
||||
template.bmp.push(colors.indexOf(bitmap[pixel]));
|
||||
}
|
||||
output.value = JSON.stringify(template);
|
||||
}
|
||||
|
||||
function templatePaint(id) {
|
||||
for (i in currentTemplate.bmp) {
|
||||
if (currentTemplate.bmp[i] == id) {
|
||||
var x = i % width;
|
||||
var y = (i - x)/width;
|
||||
pixelDraw(x, y, paint_color, paint_strength);
|
||||
}
|
||||
}
|
||||
display_bitmap();
|
||||
document.getElementById("bitmap").value = bitmap;
|
||||
}
|
||||
|
||||
var src;
|
||||
function initCanvas(paintInitData, canvasInitData) {
|
||||
initPaint(paintInitData);
|
||||
document.getElementById("paintColumn").style.maxWidth = (document.getElementById("canvas").width + 40) + "px";
|
||||
|
||||
canvasInitData = JSON.parse(canvasInitData);
|
||||
|
||||
src = canvasInitData.src;
|
||||
document.getElementById("title").value = canvasInitData.title;
|
||||
document.getElementById("author").value = canvasInitData.author;
|
||||
document.getElementById("description").value = canvasInitData.description;
|
||||
|
||||
sanitizeLength("author", "authorLengthMeter");
|
||||
sanitizeLength("title", "titleLengthMeter");
|
||||
sanitizeLength("description", "descriptionLengthMeter");
|
||||
|
||||
var paletteButtonPanel = document.getElementById("palette_buttons");
|
||||
var palette = canvasInitData.palette;
|
||||
while (paletteButtonPanel.childElementCount > 0) {
|
||||
paletteButtonPanel.removeChild(paletteButtonPanel.firstChild);
|
||||
}
|
||||
|
||||
for (color in palette) {
|
||||
paletteButtonPanel.innerHTML +=
|
||||
'<div onclick="setColor(\'' + palette[color] + '\');" style="background-image:' + generateColorPaletteBackgroundStyle(palette[color]) + '; background-image:' + generateColorPaletteBackgroundStyle(palette[color], true) + '"></div>\n';
|
||||
}
|
||||
setColor(palette[0]);
|
||||
}
|
||||
|
||||
function generateColorPaletteBackgroundStyle (color, ieMode) {
|
||||
let colorOpaque = hexToRgba(color);
|
||||
colorOpaque.a = 255;
|
||||
colorOpaque = rgbaToHex(colorOpaque);
|
||||
|
||||
// Stupid IE has to use this
|
||||
if (ieMode) {
|
||||
let ocolor = hexToRgba(color);
|
||||
return "-ms-linear-gradient(-45deg, " + colorOpaque + " 0%, " + colorOpaque + " 25%, rgba(" + ocolor.r + "," + ocolor.g + "," + ocolor.b + "," + ocolor.a/255.0 + ") 26%), url(checkerboard.png)";
|
||||
} else {
|
||||
// Sane browsers use this line
|
||||
return "linear-gradient(135deg, " + colorOpaque + " 0%, 25%, " + color + " 26%), url(checkerboard.png)";
|
||||
}
|
||||
}
|
||||
|
||||
function setColor(color){
|
||||
setPaintColor(color);
|
||||
updateSelectedColorDisplay(color, getOpacity())
|
||||
}
|
||||
|
||||
function updateSelectedColorDisplay (color, alpha) {
|
||||
color = hexToRgba(color);
|
||||
color.a = ((color.a/255.0) * alpha) * 255
|
||||
color = rgbaToHex(color)
|
||||
document.getElementById("current_color").style["background-image"] = generateColorPaletteBackgroundStyle(color);
|
||||
document.getElementById("current_color").style["background-image"] = generateColorPaletteBackgroundStyle(color, true);
|
||||
}
|
||||
|
||||
|
||||
function changeStrength(diff) {
|
||||
var strengthInput = document.getElementById("paint_strength");
|
||||
paint_strength = parseFloat(strengthInput.value, 10);
|
||||
paint_strength += diff;
|
||||
strengthInput.value = setOpacity(paint_strength);
|
||||
updateSelectedColorDisplay(getPaintColor(), strengthInput.value);
|
||||
}
|
||||
|
||||
function setStrength() {
|
||||
var strengthInput = document.getElementById("paint_strength");
|
||||
strengthInput.value = setOpacity(parseFloat(strengthInput.value, 10));
|
||||
updateSelectedColorDisplay(getPaintColor(), strengthInput.value);
|
||||
}
|
||||
|
||||
function sanitizeLength (inputId, meterId) {
|
||||
var input = document.getElementById(inputId);
|
||||
|
||||
if (input.value.length > input.maxLength)
|
||||
input.value = slice(input.value, 0, input.maxLength);
|
||||
|
||||
document.getElementById(meterId).innerHTML = "(" + input.value.length + "/" + input.maxLength + ")";
|
||||
}
|
||||
|
||||
const MAX_AUTHOR_LENGTH = 52;
|
||||
const MAX_TITLE_LENGTH = 52;
|
||||
const MAX_DESCRIPTION_LENGTH = 1024;
|
||||
|
||||
function submitData() {
|
||||
var content = "bitmap=" + encodeURIComponent(bitmap) + ";";
|
||||
content += "author=" + encodeURIComponent(document.getElementById("author").value.slice(0, MAX_AUTHOR_LENGTH)) + ";";
|
||||
content += "title=" + encodeURIComponent(document.getElementById("title").value.slice(0, MAX_TITLE_LENGTH)) + ";";
|
||||
content += "description=" + encodeURIComponent(document.getElementById("description").value.slice(0, MAX_DESCRIPTION_LENGTH));
|
||||
|
||||
HREFmultipartHandler(src, content);
|
||||
}
|
||||
150
code/modules/html_interface/paintTool/canvas.tmpl
Normal file
150
code/modules/html_interface/paintTool/canvas.tmpl
Normal file
@@ -0,0 +1,150 @@
|
||||
<input id="bitmap" type="hidden" value=""/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="mainPanel">
|
||||
<!-- Control column -->
|
||||
<div id="controlColumn" class="controlColumn">
|
||||
<!-- Navigavtion -->
|
||||
<div class="navigationBar">
|
||||
<a
|
||||
id="infoPanelButton"
|
||||
class="button linkOn"
|
||||
onclick="panelSelect('infoPanel');"
|
||||
>Info</a><a
|
||||
id="exportPanelButton"
|
||||
class="button"
|
||||
onclick="panelSelect('exportPanel');"
|
||||
>Import</a><a
|
||||
id="templatePanelButton"
|
||||
class="button"
|
||||
onclick="panelSelect('templatePanel');"
|
||||
>Template</a>
|
||||
</div>
|
||||
<hr class="line">
|
||||
|
||||
<!-- Info panel -->
|
||||
<div id="infoPanel" class="">
|
||||
<div>
|
||||
<label class="itemLabel">Title: </label><br/>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
maxlength="52"
|
||||
onkeyup="sanitizeLength('title', 'titleLengthMeter');"
|
||||
/>
|
||||
<div id="titleLengthMeter">(0/52)</div>
|
||||
</div><br>
|
||||
|
||||
<div>
|
||||
<label class="itemLabel">Author: </label><br/>
|
||||
<input
|
||||
id="author"
|
||||
type="text"
|
||||
maxlength="52"
|
||||
onkeyup="sanitizeLength('author', 'authorLengthMeter');"
|
||||
/>
|
||||
<div id="authorLengthMeter">(0/52)</div>
|
||||
</div><br>
|
||||
|
||||
<div>
|
||||
<label class="itemLabel">Description: </label><br/>
|
||||
<textarea
|
||||
id="description"
|
||||
rows="5"
|
||||
maxlength="1024"
|
||||
onkeyup="sanitizeLength('description', 'descriptionLengthMeter');"
|
||||
></textarea>
|
||||
<div id="descriptionLengthMeter">(0/1024)</div>
|
||||
</div><br>
|
||||
<a class="button" onclick="submitData();">Save changes</a>
|
||||
</div>
|
||||
|
||||
<!-- Template import/export panel -->
|
||||
<div id="exportPanel" class="hidden">
|
||||
<div class="line"><a id="importHelpButton" class="button" onclick="toggleHelp('importHelpButton', 'importHelp');">Help</a></div><br/>
|
||||
<div id="importHelp" class="hidden block">
|
||||
<p>Templates define a number of same-color regions, and assigns each pixel on your painting to a region, allowing you to use the "<i>Template</i>" tab to paint faster.</p>
|
||||
<p>Templates are formatted as a <i>JSON</i>, using the following pattern:</p>
|
||||
<code>{"rgn":[<br>
|
||||
{"clr":"#ff0000","txt":"A"},<br>
|
||||
{"clr":"#00ff00","txt":"B"},<br>
|
||||
{"clr":"#0000ff","txt":""}<br>
|
||||
], "bmp": [1,0,1,2]}</code>
|
||||
<p>The first list (<code>"rgn"</code>) is a list of regions, each defining an hexadecimal RGB color hint (<code>"clr"</code>), and some text (<code>"txt"</code>).</p>
|
||||
<p>The second list (<code>"bmp"</code>) assigns a region to each pixel, identified by their position on the <code>"rgn"</code> list: Pixels assigned to the first region will be marked with a "0", pixels assigned to the second with a "1", pixels assigned with to the third with a "2", and so on.</p>
|
||||
<p>Make sure the size of your painting's canvas matches that of the template's bitmap! A 14x14 template will look distorted on a 26x26 canvas.</p>
|
||||
<p>You may also export your current painting as a template, but be aware it may result in dozens of regions, and they will not be given any description</p>
|
||||
</div><br/>
|
||||
|
||||
<h3>Import</h3>
|
||||
<div>
|
||||
<textarea id="import-text" rows="5"></textarea>
|
||||
<a class="button" onclick="loadTemplate(document.getElementById('import-text').value)">Import template</a><br/>
|
||||
</div><br/>
|
||||
|
||||
<h3>Export</h3>
|
||||
<div>
|
||||
<textarea id="export-text" rows="5"></textarea>
|
||||
<a class="button" onclick="exportTemplate()">Export painting</a>
|
||||
</div><br/>
|
||||
</div>
|
||||
|
||||
<!-- Template panel -->
|
||||
<div id="templatePanel" class="hidden">
|
||||
<div class="line"><a id="templateHelpButton" class="button" onclick="toggleHelp('templateHelpButton', 'templateHelp');">Help</a></div><br/>
|
||||
<div id="templateHelp" class="block hidden">
|
||||
<p>Use the "<i>Import</i>" tab to import a painting template, a list of regions meant to have the same color that will be displayed on this tab.</p>
|
||||
<p>Use "<i>Paint Over</i>" to paint a given region with your currently selected color and opacity. Each region usually comes with a color hint you're meant to match (color square to the left of "<i>Paint Over</i>"), and hopefully some text explaining what each region is meant to be (info button).</p>
|
||||
<p>You may move any region to "<i>Done</i>" at any time, so as to get it out of the way if you're done with it, or back to "<i>In Progress</i>" if you need to touch it up again.</p>
|
||||
</div><br/>
|
||||
|
||||
<h3>In Progress</h3>
|
||||
<ul id="pendingRegions">
|
||||
</ul>
|
||||
|
||||
<h3>Done</h3>
|
||||
<ul id="doneRegions">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Canvas panel -->
|
||||
<div id="paintColumn" class="paintColumn">
|
||||
|
||||
<!-- Canvas -->
|
||||
<div class="canvasPanel">
|
||||
<p style="">
|
||||
If you can read this then either your version of Internet Explorer is too old (IE8 or older) or something went wrong.</p>
|
||||
<canvas
|
||||
id="canvas"
|
||||
width="280" height="280"
|
||||
onmousedown="is_mouse_down = true;"
|
||||
onmousemove="draw_on_bitmap();">
|
||||
</canvas><!--520x520-->
|
||||
</div>
|
||||
|
||||
<!-- Paint Tool Controls -->
|
||||
<div class="paintPanel">
|
||||
<span class="toolPanel">
|
||||
<label class="itemLabelNarrow">Opacity: </label>
|
||||
<a id="paint_strength_lower" class="button" onclick="changeStrength(-0.05);"><div class="uiIcon16 icon-minus"></div></a>
|
||||
<span class="statusValue" id="tool_data_inputs">
|
||||
<!-- Example. Replace with actual value using "interface.updateContent(...)"; -->
|
||||
<input type="hidden" id="min_paint_strength" value="0"/>
|
||||
<input type="hidden" id="max_paint_strength" value="1"/>
|
||||
<input type="text" id="paint_strength" value="0.5" onchange="setStrength();"/>
|
||||
</span>
|
||||
<a id="paint_strength_lower" class="button" onclick="changeStrength(0.05);"><div class="uiIcon16 icon-plus"></div></a>
|
||||
</span>
|
||||
<br/>
|
||||
<!-- Selected Color -->
|
||||
<div class="colorPanel">
|
||||
<label class="selectedColor">
|
||||
<div style="background: #000000" id="current_color"></div>
|
||||
</label>
|
||||
<!-- Color palette -->
|
||||
<div id="palette_buttons" class="palette"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
BIN
code/modules/html_interface/paintTool/checkerboard.png
Normal file
BIN
code/modules/html_interface/paintTool/checkerboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 B |
232
code/modules/html_interface/paintTool/custom_painting_datum.dm
Normal file
232
code/modules/html_interface/paintTool/custom_painting_datum.dm
Normal file
@@ -0,0 +1,232 @@
|
||||
#define PENCIL_STRENGTH_MAX 0.15
|
||||
#define PENCIL_STRENGTH_MIN 0
|
||||
#define BRUSH_STRENGTH_MAX 1
|
||||
#define BRUSH_STRENGTH_MIN 0
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* PAINTING UTENSIL DATUM
|
||||
*
|
||||
* Add any items that can be used to it's New() so it is properly converted into the right data
|
||||
*
|
||||
*/
|
||||
/datum/painting_utensil
|
||||
var/min_strength = 0
|
||||
var/max_strength = 1
|
||||
var/list/palette = list()
|
||||
var/base_color
|
||||
|
||||
/datum/painting_utensil/New(mob/user, obj/item/held_item)
|
||||
if (!user) // Special case
|
||||
return
|
||||
if (!held_item)
|
||||
held_item = user.get_active_hand()
|
||||
if (istype(held_item, /obj/item/weapon/pen))
|
||||
var/obj/item/weapon/pen/p = held_item
|
||||
max_strength = PENCIL_STRENGTH_MAX
|
||||
min_strength = PENCIL_STRENGTH_MIN
|
||||
palette += p.colour_rgb
|
||||
base_color = p.colour_rgb
|
||||
|
||||
if (istype(held_item, /obj/item/toy/crayon))
|
||||
var/obj/item/toy/crayon/c = held_item
|
||||
max_strength = PENCIL_STRENGTH_MAX
|
||||
min_strength = PENCIL_STRENGTH_MIN
|
||||
palette += c.colour
|
||||
palette += c.shadeColour
|
||||
base_color = c.color
|
||||
|
||||
if (istype(held_item, /obj/item/weapon/hair_dye))
|
||||
var/obj/item/weapon/hair_dye/h = held_item
|
||||
max_strength = PENCIL_STRENGTH_MAX
|
||||
min_strength = PENCIL_STRENGTH_MIN
|
||||
palette += rgb(h.color_r, h.color_g, h.color_b)
|
||||
base_color = rgb(h.color_r, h.color_g, h.color_b)
|
||||
|
||||
if (istype(held_item, /obj/item/weapon/painting_brush))
|
||||
var/obj/item/weapon/painting_brush/b = held_item
|
||||
if (b.paint_color)
|
||||
max_strength = BRUSH_STRENGTH_MAX
|
||||
min_strength = BRUSH_STRENGTH_MIN
|
||||
palette += b.paint_color
|
||||
base_color = b.paint_color
|
||||
|
||||
/datum/painting_utensil/proc/duplicate()
|
||||
var/datum/painting_utensil/dupe = new(null, null)
|
||||
dupe.max_strength = src.max_strength
|
||||
dupe.min_strength = src.min_strength
|
||||
dupe.palette = src.palette
|
||||
dupe.base_color = src.base_color
|
||||
dupe.tag = "\ref[dupe]"
|
||||
return dupe
|
||||
|
||||
/*
|
||||
* CUSTOM PAINTING DATUM
|
||||
*
|
||||
* Add this to any object you should be able to paint on, setting said object as this datum's parent, either through New()
|
||||
* or set_parent() if moving this datum to a different object
|
||||
*
|
||||
* Call interact() when the user starts painting
|
||||
*/
|
||||
|
||||
/datum/custom_painting
|
||||
var/parent
|
||||
|
||||
// Array listing all colors, starting from the upper left corner
|
||||
var/list/bitmap = list()
|
||||
var/bitmap_width = 14
|
||||
var/bitmap_height = 14
|
||||
|
||||
// Color that shows up on creation or after cleaning
|
||||
var/base_color = "#ffffff"
|
||||
|
||||
// Position of the lower left corner of the image when rendering the bitmap onto an icon
|
||||
var/offset_x = 0
|
||||
var/offset_y = 0
|
||||
|
||||
// UI and JS stuff
|
||||
var/datum/html_interface/interface
|
||||
var/datum/href_multipart_handler/mp_handler
|
||||
|
||||
var/author = ""
|
||||
var/title = ""
|
||||
var/description = ""
|
||||
|
||||
/datum/custom_painting/New(parent, bitmap_width, bitmap_height, offset_x=0, offset_y=0, base_color=src.base_color)
|
||||
src.parent = parent
|
||||
src.bitmap_width = bitmap_width
|
||||
src.bitmap_height = bitmap_height
|
||||
src.offset_x = offset_x
|
||||
src.offset_y = offset_y
|
||||
src.base_color = base_color
|
||||
mp_handler = new /datum/href_multipart_handler(parent)
|
||||
|
||||
blank_contents()
|
||||
setup_UI()
|
||||
|
||||
/datum/custom_painting/Destroy()
|
||||
..()
|
||||
parent = null
|
||||
|
||||
qdel(interface)
|
||||
interface = null
|
||||
|
||||
qdel(mp_handler)
|
||||
mp_handler = null
|
||||
|
||||
/datum/custom_painting/proc/Copy()
|
||||
var/datum/custom_painting/copy = new(parent, bitmap_width, bitmap_height, offset_x, offset_y, base_color)
|
||||
copy.author = author
|
||||
copy.title = title
|
||||
copy.description = description
|
||||
copy.bitmap = bitmap.Copy()
|
||||
return copy
|
||||
|
||||
/datum/custom_painting/proc/set_parent(parent)
|
||||
src.parent = parent
|
||||
mp_handler.set_parent(parent)
|
||||
|
||||
|
||||
/datum/custom_painting/proc/blank_contents()
|
||||
bitmap = list()
|
||||
for (var/i = 0, i < bitmap_height * bitmap_width, i++)
|
||||
bitmap += base_color
|
||||
|
||||
/datum/custom_painting/proc/setup_UI()
|
||||
// Setup head
|
||||
var/head = {"
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"canvas.css\" />
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"shared.css\" />
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"html_interface_icons.css\" />
|
||||
<script src=\"paintTool.js\"></script>
|
||||
<script src=\"canvas.js\"></script>
|
||||
<script src=\"href_multipart_handler.js\"></script>
|
||||
"}
|
||||
|
||||
// Use NT-style UI
|
||||
src.interface = new/datum/html_interface/nanotrasen(src, "Canvas", 600, 600, head)
|
||||
|
||||
// Setup contents
|
||||
interface.updateContent("content", file2text("code/modules/html_interface/paintTool/canvas.tmpl"))
|
||||
|
||||
/datum/custom_painting/proc/interact(mob/user, datum/painting_utensil/p)
|
||||
var/paint_init_inputs = json_encode(list(
|
||||
"width" = bitmap_width,
|
||||
"height" = bitmap_height,
|
||||
"bitmap" = bitmap,
|
||||
"minPaintStrength" = p.min_strength,
|
||||
"maxPaintStrength" = p.max_strength
|
||||
))
|
||||
|
||||
var/canvas_init_inputs = json_encode(list(
|
||||
"src" = "\ref[parent]",
|
||||
"palette" = p.palette, //list("#000000", "#ffffff", "#ff0000", "#ffff00", "#00ff00", "#00ffff", "#0000ff", "#ff00ff"),
|
||||
"title" = title,
|
||||
"author" = author,
|
||||
"description" = description
|
||||
))
|
||||
|
||||
// Send assets, wait for them to load before showing UI and initializing scripts
|
||||
var/delay = 0
|
||||
delay += send_asset(user.client, "paintTool.js")
|
||||
delay += send_asset(user.client, "canvas.js")
|
||||
delay += send_asset(user.client, "href_multipart_handler.js")
|
||||
delay += send_asset(user.client, "canvas.css")
|
||||
delay += send_asset(user.client, "checkerboard.png")
|
||||
spawn(delay)
|
||||
interface.show(user)
|
||||
interface.callJavaScript("initCanvas", list(paint_init_inputs,canvas_init_inputs), user)
|
||||
|
||||
|
||||
/datum/custom_painting/Topic(href, href_list)
|
||||
// Handle multipart href
|
||||
if (href_list["multipart"])
|
||||
mp_handler.Topic(href, href_list)
|
||||
return
|
||||
|
||||
// Save changes
|
||||
else
|
||||
// Make sure the player can actually paint
|
||||
if(!usr || usr.incapacitated())
|
||||
return
|
||||
|
||||
var /datum/painting_utensil/pu = new /datum/painting_utensil(usr)
|
||||
if(!pu.palette.len)
|
||||
//TODO other tools (crayons, brushes)
|
||||
to_chat(usr, "<span class='warning'>You need to be holding a painting utensil in your active hand.</span>")
|
||||
return
|
||||
|
||||
if (!do_after(usr, parent, 30))
|
||||
return
|
||||
|
||||
//Save and sanitize bitmap
|
||||
bitmap = splittext(url_decode(href_list["bitmap"]), ",")
|
||||
for (var/i = 1; i <= bitmap.len; i++)
|
||||
bitmap[i] = sanitize_hexcolor(bitmap[i])
|
||||
|
||||
//Save and sanitize author, title and description
|
||||
author = copytext(sanitize(url_decode(href_list["author"])), 1, MAX_NAME_LEN)
|
||||
title = copytext(sanitize(url_decode(href_list["title"])), 1, MAX_NAME_LEN)
|
||||
description = copytext(sanitize(url_decode(href_list["description"])), 1, MAX_MESSAGE_LEN)
|
||||
return TRUE
|
||||
|
||||
/datum/custom_painting/proc/render_on(icon/ico, offset_x = src.offset_x, offset_y = src.offset_y)
|
||||
var/x
|
||||
var/y
|
||||
for (var/pixel = 0; pixel < bitmap.len; pixel++)
|
||||
x = pixel % bitmap_width
|
||||
y = (pixel - x)/bitmap_width
|
||||
|
||||
//for DrawBox, (x:1,y:1) is the lower left corner. On bitmap, (x:0,y:0) is the upper left
|
||||
x = 1 + offset_x + x
|
||||
y = 1 + offset_y + bitmap_height - y
|
||||
|
||||
ico.DrawBox(bitmap[pixel + 1], x, y)
|
||||
|
||||
return ico
|
||||
|
||||
#undef PENCIL_STRENGTH_MAX
|
||||
#undef PENCIL_STRENGTH_MIN
|
||||
#undef BRUSH_STRENGTH_MAX
|
||||
#undef BRUSH_STRENGTH_MIN
|
||||
483
code/modules/html_interface/paintTool/paintTool.js
Normal file
483
code/modules/html_interface/paintTool/paintTool.js
Normal file
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* #### Painting tool ####
|
||||
*
|
||||
* A fairly simple painting tool, don't expect too many bells and whistles.
|
||||
*
|
||||
* === Requirements ===
|
||||
*
|
||||
* Your html document must contain the following:
|
||||
*
|
||||
* - 'Width': An <input> with id="width". This will determine how many pixels wide our drawing will be
|
||||
*
|
||||
* - 'Height': An <input> with id="width". This will determine how many pixels tall our drawing will be
|
||||
*
|
||||
* - 'Bitmap': An <input> with id="bitmap". It's value must be a comma separated list of colors, with no
|
||||
* spaces (eg: "#0000ff,#ff0000,#ff0000,#0000ff"). It must contain 'Width'x'Height' colors,
|
||||
* as each color will be mapped to a pixel in our drawing. It's value will be updates as the
|
||||
* user draws.
|
||||
*
|
||||
* - 'Canvas': A <canvas> with id="canvas", and the events onmousedown="is_mouse_down = true;" and
|
||||
* onmousemove="draw_on_bitmap();".
|
||||
* This will be where the user will draw and see the result. It's width and height attributes
|
||||
* should be a multiple of 'Width' and 'Height', or you'll get some visual artifacts.
|
||||
*
|
||||
* - 'Tool Strength': An <input> with id="paint_opacity". It's value must be between 1 and 0 inclusive, and
|
||||
* represents the color's "alpha value", aka how much of an effect it has in changing a
|
||||
* pixel's color.
|
||||
*
|
||||
* - 'Min. Strength': An <input> with id="minPaintStrength". It's value must be between 1 and 0 inclusive,
|
||||
* and serves to limit 'Tool Strength'
|
||||
*
|
||||
* - 'Max. Strength': An <input> with id="maxPaintStrength". It's value must be between 1 and 0 inclusive,
|
||||
* and serves to limit 'Tool Strength'
|
||||
*
|
||||
* Inputs may be of type="hidden" as needed: eg. 'Bitmap' should usually be a hidden input, unless you'd
|
||||
* like for the user to import and export it's value freely.
|
||||
*
|
||||
* === Functions ===
|
||||
*
|
||||
* --- init() ---
|
||||
*
|
||||
* Must be called as soon as all data is loaded and in place. Will load the html data (all those <input>s in the
|
||||
* requirements section) into the script.
|
||||
*
|
||||
*
|
||||
* --- setColor(color) ---
|
||||
*
|
||||
* There must be an element with id="current_color" for this function to be called.
|
||||
* Calling this function will change the color in use to the specified color, which must be a string in hex
|
||||
* format (eg: "#ffaa22"), and will update the 'current_color' element's background to said color.
|
||||
* If you'd rather forego this, you may use 'paint_color = "#ffaa22";' instead, at your own peril.
|
||||
*
|
||||
*
|
||||
* --- setStrength() ---
|
||||
*
|
||||
* Updates the tool strenght to that of the 'Tool Strenght' input, sanitizing it's value in the process. Turns
|
||||
* NaNs to 0, rounds the value to the second decimal and clamps it to the min. and max. values.
|
||||
* Useful if you wish to let the user modify the strength directly.
|
||||
*
|
||||
*
|
||||
* --- changeStrength(diff) ---
|
||||
*
|
||||
* Modifies the tool strenght by 'diff', sanitizes the result and updates the value of the 'Tool Strenght' input.
|
||||
* 'diff' should be no lower than 0.01, any values lower than that will end up ignored once the result's sanitized.
|
||||
* Useful if placing buttons to increase/decrease the tool strenght, rather than modifying it directly.
|
||||
*
|
||||
*
|
||||
* --- hexToRgba(hex), rgbaToHex(r, g, b) ---
|
||||
*
|
||||
* Helper functions to deal with rgb/hex conversions.
|
||||
* 'hexToRgba(hex)' takes eg. an "#aa88ff" string and returns an {r:170, g:136, b:255} object.
|
||||
* 'rgbaToHex(r, g, b)' takes three integers ranging from 0 to 255 and returns the corresponding string, eg. "#aa88ff"
|
||||
*/
|
||||
|
||||
//Canvas and context for the player to draw in
|
||||
var canvas;
|
||||
var ctx;
|
||||
|
||||
//Define our "bitmap"
|
||||
var width;
|
||||
var height;
|
||||
var bitmap;
|
||||
|
||||
//Keep track of how scaled up the canvas is vs the actual bitmap
|
||||
var scaleX = 20;
|
||||
var scaleY = 20;
|
||||
|
||||
//Keep track of what the mouse is up to
|
||||
var previousX = -1;
|
||||
var previousY = -1;
|
||||
var is_mouse_down = false;
|
||||
|
||||
//Color and tool data
|
||||
var paint_color = "#000000";
|
||||
var paint_opacity = 0.5;
|
||||
var minPaintStrength = 0;
|
||||
var maxPaintStrength = 1;
|
||||
|
||||
// Milliseconds to wait since we last moved the mouse in ordder to draw.
|
||||
// Ensures all browsers draw lines with the same opacity, instead of letting those that check more often drawing darker lines
|
||||
const PAINT_TOOL_MOVEMENT_THROTTLE = 10;
|
||||
|
||||
/**
|
||||
* Initialize the script
|
||||
*/
|
||||
function initPaint(initData) {
|
||||
initData = JSON.parse(initData);
|
||||
|
||||
canvas = document.getElementById("canvas");
|
||||
ctx = canvas.getContext("2d");
|
||||
|
||||
width = initData.width;
|
||||
height = initData.height;
|
||||
canvas.width = width * scaleX;
|
||||
canvas.height = height * scaleY;
|
||||
bitmap = initData.bitmap;
|
||||
|
||||
minPaintStrength = initData.minPaintStrength;
|
||||
maxPaintStrength = initData.maxPaintStrength;
|
||||
setStrength();
|
||||
|
||||
//No data? start with a blank canvas
|
||||
if (bitmap.length != width * height) {
|
||||
while (bitmap.length < width * height) {
|
||||
bitmap.push("#ffffff");
|
||||
}
|
||||
}
|
||||
|
||||
// Listener to catch any mouseup events to stop drawing
|
||||
window.addEventListener('mouseup', function(event){
|
||||
end_path();
|
||||
})
|
||||
|
||||
//Everything initialized, display the bitmap to the user for the first time.
|
||||
display_bitmap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current color to the specified color, updating the "selected color" display
|
||||
*/
|
||||
function setPaintColor(color) {
|
||||
paint_color = color;
|
||||
}
|
||||
|
||||
function getPaintColor() {
|
||||
return paint_color;
|
||||
}
|
||||
|
||||
function setOpacity(opacity) {
|
||||
paint_opacity = isNaN(opacity) ? 0 : opacity;
|
||||
paint_opacity = Math.round(paint_opacity*100)/100;
|
||||
paint_opacity = Math.min(Math.max(paint_opacity, minPaintStrength), maxPaintStrength);
|
||||
return paint_opacity;
|
||||
}
|
||||
|
||||
function getOpacity() {
|
||||
return paint_opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex to RGBA objects.
|
||||
* Helper function, converts an hex string (eg: #AA88FF or #AA88FFFF) to an
|
||||
* {r:170, g:136, b:255, a:255} object.
|
||||
* If the alpha component is missing it'll be treated as FF
|
||||
*/
|
||||
function hexToRgba(hex) {
|
||||
//Pad with alpha component if missing
|
||||
hex = hex.slice(1);
|
||||
if (hex.length < 8) hex += "FF";
|
||||
|
||||
//Get rid of '#'
|
||||
hex = parseInt(hex, 16);
|
||||
|
||||
//Bitwise magic
|
||||
return {
|
||||
r: (hex >> 24) & 255,
|
||||
g: (hex >> 16) & 255,
|
||||
b: (hex >> 8) & 255,
|
||||
a: hex & 255
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGBA to hex.
|
||||
* Helper function, converts {r, g, b, a} objects (eg: {r:170, g:136, b:255, r:170}) to an
|
||||
* hex string (eg: #AA88FFAA).
|
||||
* If the alpha component is FF, it will be omitted on the hex
|
||||
*/
|
||||
function rgbaToHex(rgba) {
|
||||
for (k in rgba) {
|
||||
//Convert to hex value
|
||||
rgba[k] = Math.round(rgba[k]).toString(16);
|
||||
|
||||
//Pad with 0 if needed
|
||||
rgba[k] = rgba[k].length > 1 ? rgba[k] : "0" + rgba[k];
|
||||
}
|
||||
|
||||
//Put it together
|
||||
return "#" + rgba.r + rgba.g + rgba.b + (rgba.a != "ff" ? rgba.a : "")
|
||||
}
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------------------------------
|
||||
*
|
||||
*/
|
||||
|
||||
var blendFunction = colorRybBlend;
|
||||
|
||||
/**
|
||||
* Draw a pixel into the bitmap.
|
||||
* Given 'rgba' as an hex string (eg: #AA88FF) and 'a' (alpha) as a 0-1 value, will mix said color
|
||||
* with whatever's on the specified pixel on the bitmap.
|
||||
*/
|
||||
function pixelDraw(x, y, rgba, alpha) {
|
||||
//Figure out the pixel index off the x and y
|
||||
let pixel = y * width + x;
|
||||
|
||||
//Convert to numeric values
|
||||
rgba = hexToRgba(rgba);
|
||||
let orgba = hexToRgba(bitmap[pixel]);
|
||||
|
||||
//Mix both color values
|
||||
rgba = blendFunction(rgba, orgba, alpha);
|
||||
|
||||
//Save result into bitmap
|
||||
bitmap[pixel] = rgbaToHex(rgba);
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
Color blends
|
||||
Mostly based on:
|
||||
https://www.w3.org/TR/compositing-1/#valdef-blend-mode-hard-light
|
||||
*/
|
||||
function colorAlphaBlend(c1, c2, alpha) {
|
||||
var result = {};
|
||||
for (k in c1)
|
||||
result[k] = Math.round(Math.sqrt(alpha * Math.pow(c1[k], 2) + (1-alpha) * Math.pow(c2[k], 2)));
|
||||
return result;
|
||||
}
|
||||
|
||||
function colorMultiplyBlend(c1, c2, alpha) {
|
||||
var result = {};
|
||||
for (k in c1)
|
||||
result[k] = Math.round(c1[k]*c2[k]/255);
|
||||
return colorAlphaBlend(result, c2, alpha);
|
||||
}
|
||||
|
||||
function colorScreenBlend(c1, c2, alpha) {
|
||||
var result = {};
|
||||
for (k in c1)
|
||||
result[k] = Math.round(c1[k] + c2[k] - c1[k]*c2[k]/255);
|
||||
return colorAlphaBlend(result, c2, alpha);
|
||||
}
|
||||
|
||||
function colorHardLightBlend(c1, c2, alpha) {
|
||||
var result = {r: c1.r * 2, g: c1.g * 2, b: c1.b * 2};
|
||||
for (var c in c1) {
|
||||
if (c1[c] <= 127.5) {
|
||||
result[c] = Math.round(result[c]*c2[c]/255);
|
||||
} else {
|
||||
result[c] -= 255;
|
||||
result[c] = Math.round((result[c] + c2[c] - result[c]*c2[c]/255));
|
||||
}
|
||||
}
|
||||
return colorAlphaBlend(result, c2, alpha);
|
||||
}
|
||||
|
||||
function colorOverlayBlend(c1, c2, alpha) {
|
||||
return colorHardLightBlend(c2, c1, alpha);
|
||||
}
|
||||
|
||||
function colorRybBlend(c1, c2, alpha) {
|
||||
var c1Ryb = rgbToRyb(c1);
|
||||
var c2Ryb = rgbToRyb(c2);
|
||||
var resultRyb = {r:0, y:0, b:0, a:c2Ryb.a};
|
||||
|
||||
alpha *= c1Ryb.a / 255.0;
|
||||
|
||||
resultRyb.r = Math.round(alpha * c1Ryb.r + (1-alpha) * c2Ryb.r);
|
||||
resultRyb.y = Math.round(alpha * c1Ryb.y + (1-alpha) * c2Ryb.y);
|
||||
resultRyb.b = Math.round(alpha * c1Ryb.b + (1-alpha) * c2Ryb.b);
|
||||
return rybToRgb(resultRyb);
|
||||
}
|
||||
|
||||
/*---------------------------------------------
|
||||
end color blends
|
||||
*/
|
||||
|
||||
/**
|
||||
* RGB to RYB (red, yellow, blue) converter
|
||||
* Takes a RGB color object such as {r:40, g:15, b:90} and returns an RYB object such
|
||||
* as {r:215, y:165, b:240}.
|
||||
* Formula based on the following papers:
|
||||
* - http://nishitalab.org/user/UEI/publication/Sugita_IWAIT2015.pdf
|
||||
* - http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf
|
||||
*/
|
||||
function rgbToRyb(rgb) {
|
||||
// Soon-to-be result
|
||||
var ryb = {r:0, y:0, b:0, a:rgb.a};
|
||||
|
||||
// Make a copy of the input to work on
|
||||
var tmpRgb = {r: rgb.r, g: rgb.g, b: rgb.b};
|
||||
|
||||
// Remove white component
|
||||
var i = Math.min(rgb.r, rgb.g, rgb.b);
|
||||
tmpRgb.r -= i;
|
||||
tmpRgb.g -= i;
|
||||
tmpRgb.b -= i;
|
||||
|
||||
// Convert colors
|
||||
ryb.r = tmpRgb.r - Math.min(tmpRgb.r, tmpRgb.g);
|
||||
ryb.y = (tmpRgb.g + Math.min(tmpRgb.r, tmpRgb.g))/2;
|
||||
ryb.b = (tmpRgb.b + tmpRgb.g - Math.min(tmpRgb.r, tmpRgb.g))/2;
|
||||
|
||||
// Normalize
|
||||
var n = Math.max(ryb.r, ryb.y, ryb.b)/Math.max(tmpRgb.r, tmpRgb.g, tmpRgb.b);
|
||||
if (n > 0.000001) { // Should be zero, but floating point error could be an issue
|
||||
ryb.r /= n;
|
||||
ryb.y /= n;
|
||||
ryb.b /= n;
|
||||
}
|
||||
|
||||
// Add black component, and round floating point errors
|
||||
i = Math.min(255 - rgb.r, 255 - rgb.g, 255 - rgb.b);
|
||||
ryb.r = Math.round(ryb.r + i);
|
||||
ryb.y = Math.round(ryb.y + i);
|
||||
ryb.b = Math.round(ryb.b + i);
|
||||
|
||||
return ryb;
|
||||
}
|
||||
|
||||
/**
|
||||
* RYB (red, yellow, blue) to RGB converter
|
||||
* Takes a RYB color object such as {r:215, y:165, b:240} and returns an RGB object such
|
||||
* as {r:40, g:15, b:90}.
|
||||
* Formula based on the following papers:
|
||||
* - http://nishitalab.org/user/UEI/publication/Sugita_IWAIT2015.pdf
|
||||
* - http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf
|
||||
*/
|
||||
function rybToRgb(ryb) {
|
||||
// Soon-to-be result
|
||||
var rgb = {r:0, g:0, b:0, a:ryb.a};
|
||||
|
||||
// Make a copy of the input to work on
|
||||
var tmpRyb = {r: ryb.r, y: ryb.y, b: ryb.b};
|
||||
|
||||
// Remove black component
|
||||
var i = Math.min(ryb.r, ryb.y, ryb.b);
|
||||
tmpRyb.r -= i;
|
||||
tmpRyb.y -= i;
|
||||
tmpRyb.b -= i;
|
||||
|
||||
// Convert colors
|
||||
rgb.r = tmpRyb.r + tmpRyb.y - Math.min(tmpRyb.y, tmpRyb.b);
|
||||
rgb.g = tmpRyb.y + Math.min(tmpRyb.y, tmpRyb.b);
|
||||
rgb.b = 2*(tmpRyb.b - Math.min(tmpRyb.y, tmpRyb.b));
|
||||
/* According to the RYB papers linked, the formula for green should be
|
||||
* "g = y + 2*min(y, b)"
|
||||
* But for whatever godforsaken reason that returns wrong values for colors where y < b
|
||||
* (eg: cyan). Got rid of the '2*' on a hunch and sure it WORKS without breaking anything
|
||||
* else, but WHY?????
|
||||
*/
|
||||
|
||||
// Normalize
|
||||
var n = Math.max(rgb.r, rgb.g, rgb.b)/Math.max(tmpRyb.r, tmpRyb.y, tmpRyb.b);
|
||||
if (n > 0.000001) { // Should be zero, but floating point error could be an issue
|
||||
rgb.r /= n;
|
||||
rgb.g /= n;
|
||||
rgb.b /= n;
|
||||
}
|
||||
|
||||
// Add white component, and round floating point errors
|
||||
i = Math.min(255 - ryb.r, 255 - ryb.y, 255 - ryb.b);
|
||||
rgb.r = Math.round(rgb.r + i);
|
||||
rgb.g = Math.round(rgb.g + i);
|
||||
rgb.b = Math.round(rgb.b + i);
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listen to mouse actions and draw.
|
||||
* Gets called whenever the mouse either moves within the canvas or is released from being
|
||||
* pressed down (eg. single clicks)
|
||||
*/
|
||||
|
||||
var lastMove = 0;
|
||||
function draw_on_bitmap() {
|
||||
//If the mouse is pressed down and inside the canvas...
|
||||
if (is_mouse_down
|
||||
&& event.offsetX > 0 && event.offsetX < canvas.width
|
||||
&& event.offsetY > 0 && event.offsetY < canvas.height
|
||||
&& (Date.now() - lastMove) > PAINT_TOOL_MOVEMENT_THROTTLE)
|
||||
{
|
||||
//Translate mouse position to bitmap position
|
||||
var x = Math.floor(width * event.offsetX/canvas.width);
|
||||
var y = Math.floor(height * event.offsetY/canvas.height);
|
||||
|
||||
//If the mouse moves too fast, "skipping" pixels, fill the gap by drawing a line
|
||||
// between it and the last recorded position
|
||||
if (previousX > -1 && (Math.abs(previousX - x) > 1 || Math.abs(previousY - y) > 1 )) {
|
||||
lineDraw(previousX, previousY, x, y, paint_color, paint_opacity);
|
||||
}
|
||||
|
||||
//Draw a pixel wherever we're at
|
||||
pixelDraw(x, y, paint_color, paint_opacity);
|
||||
|
||||
//Record our current position as last recorded
|
||||
previousX = x;
|
||||
previousY = y;
|
||||
|
||||
// Record the time of our last movement, for throttling
|
||||
lastMove = Date.now();
|
||||
|
||||
//Update the UI
|
||||
display_bitmap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on mouse no longer being pressed down.
|
||||
* Draws a single pixel if we just did a single click. Clears the last recorded position so
|
||||
* future clicks aren't treated as incredibly fast movements.
|
||||
*/
|
||||
function end_path () {
|
||||
if (previousX == -1)
|
||||
draw_on_bitmap();
|
||||
is_mouse_down = false;
|
||||
previousX = -1;
|
||||
previousY = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line between two points, neither point included.
|
||||
* Sensitive both to mouse movement and the mouse button being released.
|
||||
*/
|
||||
function lineDraw(x1, y1, x2, y2, rgb, a) {
|
||||
//Difference in "steps" between both axes
|
||||
var sx = x2 - x1;
|
||||
var sy = y2 - y1;
|
||||
|
||||
//Figure out how much to advance between steps, and how many steps to take (sx or sy, whichever is greater)
|
||||
var dx;
|
||||
var dy;
|
||||
var steps = 0;
|
||||
if (Math.abs(sx) > Math.abs(sy)) {
|
||||
steps = Math.abs(sx);
|
||||
dx = sx/Math.abs(sx);//Either 1 or -1
|
||||
dy = sy/Math.abs(sx);
|
||||
} else if (sy != 0){
|
||||
steps = Math.abs(sy);
|
||||
dx = sx/Math.abs(sy);
|
||||
dy = sy/Math.abs(sy);//Either 1 or -1
|
||||
}
|
||||
|
||||
//Move however many steps we decided on, starting from x1 and y1 and increasing both by dx and dy each step
|
||||
//Skip the first and last step though, draw_on_bitmap() already handles those
|
||||
for (var i = 1; i < steps; i++) {
|
||||
//Result might look like x:0.1 y:1, x:0.2 y:2.. Decimals are not possible, so round it
|
||||
pixelDraw(x1 + Math.round(i*dx), y1 + Math.round(i*dy), rgb, a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the bitmap to the player
|
||||
* Draws the bitmap's contents on screen, scaled up for visibility
|
||||
*/
|
||||
function display_bitmap() {
|
||||
//Go through our pixel data and draw scaled up squares with the corresponding color
|
||||
for (var x = 0; x < width; x++) {
|
||||
for(var y = 0; y < height; y++) {
|
||||
//Convert to pixel index
|
||||
var pixel = (y * width + x);
|
||||
|
||||
//Grab the pixel's color
|
||||
ctx.fillStyle = bitmap[pixel];
|
||||
|
||||
//Draw a square, scaled up as needed
|
||||
ctx.fillRect(x*scaleX, y*scaleY, scaleX, scaleY);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user