mirror of
https://github.com/tsl0922/EPD-nRF5.git
synced 2025-12-05 15:32:48 +08:00
update html
This commit is contained in:
@@ -20,79 +20,125 @@ body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.debug {
|
||||
display: none !important;
|
||||
button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
body.debug-mode .debug {
|
||||
display: flex !important;
|
||||
button:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
body.debug-mode {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--dark-text);
|
||||
button.primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
body.debug-mode .main {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--dark-text);
|
||||
button.primary:hover {
|
||||
color: #fff;
|
||||
border-color: var(--primary-hover);
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
body.debug-mode fieldset {
|
||||
background-color: var(--dark-fieldset-bg);
|
||||
box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.5);
|
||||
button.secondary {
|
||||
color: #fff;
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
body.debug-mode h3 {
|
||||
border-bottom: 1px solid var(--dark-border);
|
||||
color: var(--dark-text);
|
||||
button.secondary:hover {
|
||||
color: #fff;
|
||||
border-color: var(--secondary-hover);
|
||||
background-color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
body.debug-mode code {
|
||||
background: var(--dark-code-bg);
|
||||
color: #ff9800;
|
||||
h3 {
|
||||
padding-bottom: .3em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.debug-mode #log {
|
||||
background: var(--dark-log-bg);
|
||||
border: 1px solid var(--dark-border);
|
||||
fieldset {
|
||||
border: none;
|
||||
box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.2);
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body.debug-mode #log .time {
|
||||
color: #8bc34a;
|
||||
fieldset legend {
|
||||
font-weight: bold;
|
||||
color: rgba(0, 0, 255, 0.6);
|
||||
}
|
||||
|
||||
body.debug-mode #log .action {
|
||||
color: #03a9f4;
|
||||
code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body.debug-mode input[type=text],
|
||||
body.debug-mode input[type=number],
|
||||
body.debug-mode select {
|
||||
background-color: var(--dark-input-bg);
|
||||
color: var(--dark-input-text);
|
||||
border-color: var(--dark-border);
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
select {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: .2rem .75rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.debug-mode input[type=file] {
|
||||
color: var(--dark-input-text);
|
||||
background-color: transparent;
|
||||
border-color: var(--dark-border);
|
||||
input[type=file] {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.debug-mode input[type=file]::file-selector-button {
|
||||
background-color: var(--dark-fieldset-bg);
|
||||
color: var(--dark-input-text);
|
||||
border-color: var(--dark-border);
|
||||
input::file-selector-button {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.debug-mode input[type=file]::file-selector-button:hover {
|
||||
background-color: #333;
|
||||
border-color: #444;
|
||||
select {
|
||||
padding: .3rem 2.25rem .3rem .75rem;
|
||||
}
|
||||
|
||||
body.debug-mode fieldset legend {
|
||||
color: #64b5f6;
|
||||
input:focus,
|
||||
select:focus {
|
||||
border: 1px solid #86b7fe;
|
||||
box-shadow: 0 0 4px rgba(0, 120, 215, 0.8);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input[type=text]:disabled,
|
||||
input[type=number]:disabled,
|
||||
select:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
background-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -144,50 +190,6 @@ body.debug-mode fieldset legend {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body.debug-mode .footer .links a:not(:last-child)::after {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
body.debug-mode .footer {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body.debug-mode .footer a {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body.debug-mode .footer a:hover {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-bottom: .3em;
|
||||
border-bottom: 1px solid #CCC;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.2);
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
fieldset legend {
|
||||
font-weight: bold;
|
||||
color: rgba(0, 0, 255, 0.6);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background: #CCC;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -206,17 +208,17 @@ code {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin: 10px 0;
|
||||
.debug {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#log {
|
||||
.log-container {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
max-height: 300px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
background: #DDD;
|
||||
background: #ddd;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
@@ -224,32 +226,27 @@ code {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#log div {
|
||||
.log-container .log-line {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
#log .time,
|
||||
#log .action {
|
||||
.log-container .time,
|
||||
.log-container .action {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#log .time {
|
||||
.log-container .time {
|
||||
color: #333;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
#log .action {
|
||||
.log-container .action {
|
||||
color: #666;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
#canvas-box {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
.canvas-container canvas {
|
||||
border: black solid 1px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
@@ -257,95 +254,11 @@ code {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.canvas-container.crop-mode #canvas {
|
||||
.canvas-container.crop-mode canvas {
|
||||
border: 2px dashed var(--primary-color);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
color: #fff;
|
||||
border-color: var(--primary-hover);
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
color: #fff;
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
color: #fff;
|
||||
border-color: var(--secondary-hover);
|
||||
background-color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
select {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: .2rem .75rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input::file-selector-button {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: .3rem 2.25rem .3rem .75rem;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
border: 1px solid #86b7fe;
|
||||
box-shadow: 0 0 4px rgba(0, 120, 215, 0.8);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: none;
|
||||
font-size: 85%;
|
||||
@@ -357,7 +270,7 @@ label {
|
||||
|
||||
canvas.text-placement-mode {
|
||||
border: 2px dashed var(--primary-color) !important;
|
||||
cursor: text !important; /* Force text cursor */
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
.canvas-title {
|
||||
@@ -423,44 +336,11 @@ canvas.text-placement-mode {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.debug-mode .tool-button {
|
||||
background-color: var(--dark-input-bg);
|
||||
border-color: var(--dark-border);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.debug-mode .tool-button:hover {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body.debug-mode .tool-button.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
input[type=text]:disabled,
|
||||
input[type=number]:disabled,
|
||||
select:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
background-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
body.debug-mode input[type=text]:disabled,
|
||||
body.debug-mode input[type=number]:disabled,
|
||||
body.debug-mode select:disabled {
|
||||
background-color: #1a1a1a;
|
||||
color: #666;
|
||||
border-color: #2a2a2a;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flex-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-container.options .flex-group label {
|
||||
min-width: 80px;
|
||||
}
|
||||
@@ -480,7 +360,7 @@ body.debug-mode select:disabled {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#log {
|
||||
.log-container {
|
||||
height: 150px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -499,4 +379,112 @@ body.debug-mode select:disabled {
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
body.dark-mode .debug {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
body.dark-mode,
|
||||
body.dark-mode .main {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-mode fieldset {
|
||||
background-color: var(--dark-fieldset-bg);
|
||||
box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body.dark-mode h3 {
|
||||
border-bottom: 1px solid var(--dark-border);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-mode code {
|
||||
background: var(--dark-code-bg);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
body.dark-mode input[type=text],
|
||||
body.dark-mode input[type=number],
|
||||
body.dark-mode select {
|
||||
background-color: var(--dark-input-bg);
|
||||
color: var(--dark-input-text);
|
||||
border-color: var(--dark-border);
|
||||
}
|
||||
|
||||
body.dark-mode input[type=text]:disabled,
|
||||
body.dark-mode input[type=number]:disabled,
|
||||
body.dark-mode select:disabled {
|
||||
background-color: #1a1a1a;
|
||||
color: #666;
|
||||
border-color: #2a2a2a;
|
||||
}
|
||||
|
||||
body.dark-mode input[type=file] {
|
||||
color: var(--dark-input-text);
|
||||
background-color: transparent;
|
||||
border-color: var(--dark-border);
|
||||
}
|
||||
|
||||
body.dark-mode input[type=file]::file-selector-button {
|
||||
background-color: var(--dark-fieldset-bg);
|
||||
color: var(--dark-input-text);
|
||||
border-color: var(--dark-border);
|
||||
}
|
||||
|
||||
body.dark-mode input[type=file]::file-selector-button:hover {
|
||||
background-color: #333;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body.dark-mode .log-container {
|
||||
background: var(--dark-log-bg);
|
||||
border: 1px solid var(--dark-border);
|
||||
}
|
||||
|
||||
body.dark-mode .log-container .time {
|
||||
color: #8bc34a;
|
||||
}
|
||||
|
||||
body.dark-mode .log-container .action {
|
||||
color: #03a9f4;
|
||||
}
|
||||
|
||||
body.dark-mode fieldset legend {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
body.dark-mode .footer .links a:not(:last-child)::after {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
body.dark-mode .footer {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body.dark-mode .footer a {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body.dark-mode .footer a:hover {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
body.dark-mode .tool-button {
|
||||
background-color: var(--dark-input-bg);
|
||||
border-color: var(--dark-border);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
body.dark-mode .tool-button:hover {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body.dark-mode .tool-button.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
137
html/index.html
137
html/index.html
@@ -1,24 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>墨水屏日历</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>墨水屏日历</title>
|
||||
<link rel="shortcut icon" type="image/png" href="favicon.png">
|
||||
<link rel="stylesheet" href="css/main.css?v=20250731">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<h3>墨水屏日历</h3>
|
||||
<fieldset>
|
||||
<legend>蓝牙连接</legend>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<button id="connectbutton" type="button" class="primary" onclick="preConnect()">连接</button>
|
||||
<button id="reconnectbutton" type="button" class="secondary" onclick="reConnect()">重连</button>
|
||||
<div class="main">
|
||||
<h3>墨水屏日历</h3>
|
||||
<fieldset>
|
||||
<legend>蓝牙连接</legend>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<button id="connectbutton" type="button" class="primary" onclick="preConnect()">连接</button>
|
||||
<button id="reconnectbutton" type="button" class="secondary" onclick="reConnect()">重连</button>
|
||||
<button type="button" class="secondary" onclick="clearLog()">清空日志</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-group right debug">
|
||||
<label for="epddriver">驱动</label>
|
||||
<select id="epddriver">
|
||||
@@ -41,25 +42,25 @@
|
||||
<input id="epdpins" type="text" value="">
|
||||
<button id="setDriverbutton" type="button" class="primary" onclick="setDriver()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="log-container" id="log"></div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>设备控制</legend>
|
||||
<div class="flex-container">
|
||||
<legend>设备控制</legend>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<button id="calendarmodebutton" type="button" class="primary" onclick="syncTime(1)">日历模式</button>
|
||||
<button id="clockmodebutton" type="button" class="primary" onclick="syncTime(2)">时钟模式</button>
|
||||
<button id="calendarmodebutton" type="button" class="primary" onclick="syncTime(1)">日历模式</button>
|
||||
<button id="clockmodebutton" type="button" class="primary" onclick="syncTime(2)">时钟模式</button>
|
||||
<button id="clearscreenbutton" type="button" class="secondary" onclick="clearScreen()">清除屏幕</button>
|
||||
</div>
|
||||
<div class="flex-group right debug">
|
||||
<input type="text" id="cmdTXT" value="">
|
||||
<button id="sendcmdbutton" type="button" class="primary" onclick="sendcmd()">发送命令</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-group right debug">
|
||||
<input type="text" id="cmdTXT" value="">
|
||||
<button id="sendcmdbutton" type="button" class="primary" onclick="sendcmd()">发送命令</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>蓝牙传图</legend>
|
||||
<legend>蓝牙传图</legend>
|
||||
<div class="flex-container">
|
||||
<input type="file" id="imageFile" accept=".png,.jpg,.bmp,.webp,.jpeg">
|
||||
</div>
|
||||
@@ -154,13 +155,13 @@
|
||||
<div class="flex-group brush-tools">
|
||||
<label for="brush-color">颜色:</label>
|
||||
<select id="brush-color">
|
||||
<option value="#000000">黑色</option>
|
||||
<option value="#FF0000">红色</option>
|
||||
<option value="#FFFF00">黄色</option>
|
||||
<option value="#000000">黑色</option>
|
||||
<option value="#FF0000">红色</option>
|
||||
<option value="#FFFF00">黄色</option>
|
||||
<option value="#00FF00">绿色</option>
|
||||
<option value="#0000FF">蓝色</option>
|
||||
<option value="#FFFFFF">白色</option>
|
||||
</select>
|
||||
<option value="#FFFFFF">白色</option>
|
||||
</select>
|
||||
<label for="brush-size">粗细:</label>
|
||||
<input type="number" id="brush-size" value="2" min="1" max="100">
|
||||
</div>
|
||||
@@ -169,34 +170,34 @@
|
||||
<div class="flex-group text-tools">
|
||||
<label for="font-family">字体:</label>
|
||||
<select id="font-family">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="sans-serif">Sans-serif</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
<option value="SimSun">宋体</option>
|
||||
<option value="SimHei">黑体</option>
|
||||
<option value="Microsoft Yahei">微软雅黑</option>
|
||||
<option value="Microsoft JhengHei">微软正黑体</option>
|
||||
<option value="KaiTi">楷体</option>
|
||||
<option value="NSimSun">新宋体</option>
|
||||
<option value="FangSong">仿宋</option>
|
||||
<option value="YouYuan">幼圆</option>
|
||||
<option value="LiSu">隶书</option>
|
||||
<option value="STHeiti">华文黑体</option>
|
||||
<option value="STXihei">华文细黑</option>
|
||||
<option value="STKaiti">华文楷体</option>
|
||||
<option value="STSong">华文宋体</option>
|
||||
<option value="STFangsong">华文仿宋</option>
|
||||
<option value="STZhongsong">华文中宋</option>
|
||||
<option value="STHupo">华文琥珀</option>
|
||||
<option value="STXinwei">华文新魏</option>
|
||||
<option value="STLiti">华文隶书</option>
|
||||
<option value="STXingkai">华文行楷</option>
|
||||
<option value="FZShuTi">方正舒体</option>
|
||||
<option value="FZYaoti">方正姚体</option>
|
||||
<option value="PingFang SC">苹方</option>
|
||||
<option value="Source Han Sans CN">思源黑体</option>
|
||||
<option value="Source Han Serif SC">思源宋体</option>
|
||||
<option value="WenQuanYi Micro Hei">文泉驿微米黑</option>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="sans-serif">Sans-serif</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
<option value="SimSun">宋体</option>
|
||||
<option value="SimHei">黑体</option>
|
||||
<option value="Microsoft Yahei">微软雅黑</option>
|
||||
<option value="Microsoft JhengHei">微软正黑体</option>
|
||||
<option value="KaiTi">楷体</option>
|
||||
<option value="NSimSun">新宋体</option>
|
||||
<option value="FangSong">仿宋</option>
|
||||
<option value="YouYuan">幼圆</option>
|
||||
<option value="LiSu">隶书</option>
|
||||
<option value="STHeiti">华文黑体</option>
|
||||
<option value="STXihei">华文细黑</option>
|
||||
<option value="STKaiti">华文楷体</option>
|
||||
<option value="STSong">华文宋体</option>
|
||||
<option value="STFangsong">华文仿宋</option>
|
||||
<option value="STZhongsong">华文中宋</option>
|
||||
<option value="STHupo">华文琥珀</option>
|
||||
<option value="STXinwei">华文新魏</option>
|
||||
<option value="STLiti">华文隶书</option>
|
||||
<option value="STXingkai">华文行楷</option>
|
||||
<option value="FZShuTi">方正舒体</option>
|
||||
<option value="FZYaoti">方正姚体</option>
|
||||
<option value="PingFang SC">苹方</option>
|
||||
<option value="Source Han Sans CN">思源黑体</option>
|
||||
<option value="Source Han Serif SC">思源宋体</option>
|
||||
<option value="WenQuanYi Micro Hei">文泉驿微米黑</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<label for="font-size">大小:</label>
|
||||
@@ -213,13 +214,13 @@
|
||||
<button id="crop-zoom-out" title="缩小" class="secondary">-</button>
|
||||
<button id="crop-move-left" title="左移">⇦</button>
|
||||
<button id="crop-move-up" title="上移">⇧</button>
|
||||
<button id="crop-move-down" title="下移">⇩</button>
|
||||
<button id="crop-move-right" title="右移">⇨</button>
|
||||
<button class="primary" onclick="finishCrop()">完成</button>
|
||||
</div>
|
||||
<button id="crop-move-down" title="下移">⇩</button>
|
||||
<button id="crop-move-right" title="右移">⇨</button>
|
||||
<button class="primary" onclick="cropManager.finishCrop()">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="footer">
|
||||
<span class="copy">© 2025 tsl0922.</span>
|
||||
<span class="links">
|
||||
@@ -228,11 +229,11 @@
|
||||
<a href="?debug=true" id="debug-toggle">开发模式</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="js/dithering.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/paint.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/crop.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/main.js?v=20250731"></script>
|
||||
</div>
|
||||
<script type="text/javascript" src="js/dithering.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/paint.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/crop.js?v=20250731"></script>
|
||||
<script type="text/javascript" src="js/main.js?v=20250731"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
390
html/js/crop.js
390
html/js/crop.js
@@ -1,208 +1,224 @@
|
||||
let backgroundZoom = 1;
|
||||
let backgroundPanX = 0;
|
||||
let backgroundPanY = 0;
|
||||
let isPanning = false;
|
||||
let lastPanX = 0;
|
||||
let lastPanY = 0;
|
||||
let lastTouchDistance = 0;
|
||||
class CropManager {
|
||||
constructor(canvas, ctx, paintManager) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.paintManager = paintManager;
|
||||
this.backgroundZoom = 1;
|
||||
this.backgroundPanX = 0;
|
||||
this.backgroundPanY = 0;
|
||||
this.isPanning = false;
|
||||
this.lastPanX = 0;
|
||||
this.lastPanY = 0;
|
||||
this.lastTouchDistance = 0;
|
||||
|
||||
function resetCropStates() {
|
||||
backgroundZoom = 1;
|
||||
backgroundPanX = 0;
|
||||
backgroundPanY = 0;
|
||||
isPanning = false;
|
||||
lastPanX = 0;
|
||||
lastPanY = 0;
|
||||
}
|
||||
|
||||
function isCropMode() {
|
||||
return canvas.parentNode.classList.contains('crop-mode');
|
||||
}
|
||||
|
||||
function exitCropMode() {
|
||||
canvas.parentNode.classList.remove('crop-mode');
|
||||
setCanvasTitle("");
|
||||
|
||||
canvas.removeEventListener('wheel', handleBackgroundZoom);
|
||||
canvas.removeEventListener('mousedown', handleBackgroundPanStart);
|
||||
canvas.removeEventListener('mousemove', handleBackgroundPan);
|
||||
canvas.removeEventListener('mouseup', handleBackgroundPanEnd);
|
||||
canvas.removeEventListener('mouseleave', handleBackgroundPanEnd);
|
||||
canvas.removeEventListener('touchstart', handleTouchStart);
|
||||
canvas.removeEventListener('touchmove', handleTouchMove);
|
||||
canvas.removeEventListener('touchend', handleBackgroundPanEnd);
|
||||
canvas.removeEventListener('touchcancel', handleBackgroundPanEnd);
|
||||
}
|
||||
|
||||
function initializeCrop() {
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
if (imageFile.files.length == 0) {
|
||||
fillCanvas('white');
|
||||
return;
|
||||
// Bind event handlers
|
||||
this.handleBackgroundZoom = this.handleBackgroundZoom.bind(this);
|
||||
this.handleBackgroundPanStart = this.handleBackgroundPanStart.bind(this);
|
||||
this.handleBackgroundPan = this.handleBackgroundPan.bind(this);
|
||||
this.handleBackgroundPanEnd = this.handleBackgroundPanEnd.bind(this);
|
||||
this.handleTouchStart = this.handleTouchStart.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
}
|
||||
|
||||
exitCropMode();
|
||||
resetCropStates();
|
||||
|
||||
canvas.style.backgroundImage = `url(${URL.createObjectURL(imageFile.files[0])})`;
|
||||
canvas.style.backgroundSize = '100%';
|
||||
canvas.style.backgroundPosition = '';
|
||||
canvas.style.backgroundRepeat = 'no-repeat';
|
||||
|
||||
// add event listeners for zoom and pan
|
||||
canvas.addEventListener('wheel', handleBackgroundZoom);
|
||||
canvas.addEventListener('mousedown', handleBackgroundPanStart);
|
||||
canvas.addEventListener('mousemove', handleBackgroundPan);
|
||||
canvas.addEventListener('mouseup', handleBackgroundPanEnd);
|
||||
canvas.addEventListener('mouseleave', handleBackgroundPanEnd);
|
||||
|
||||
// Touch events for mobile devices
|
||||
canvas.addEventListener('touchstart', handleTouchStart);
|
||||
canvas.addEventListener('touchmove', handleTouchMove);
|
||||
canvas.addEventListener('touchend', handleBackgroundPanEnd);
|
||||
canvas.addEventListener('touchcancel', handleBackgroundPanEnd);
|
||||
|
||||
// Make the canvas transparent
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
setCanvasTitle("裁剪模式: 可用鼠标滚轮或双指触摸缩放图片");
|
||||
canvas.parentNode.classList.add('crop-mode');
|
||||
}
|
||||
|
||||
function finishCrop() {
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
if (imageFile.files.length == 0) return;
|
||||
|
||||
const image = new Image();
|
||||
image.onload = function () {
|
||||
URL.revokeObjectURL(this.src);
|
||||
|
||||
const fieldsetRect = canvas.getBoundingClientRect();
|
||||
const scale = (image.width / fieldsetRect.width) / backgroundZoom;
|
||||
|
||||
const sx = -backgroundPanX * scale;
|
||||
const sy = -backgroundPanY * scale;
|
||||
const sWidth = fieldsetRect.width * scale;
|
||||
const sHeight = fieldsetRect.height * scale;
|
||||
|
||||
fillCanvas('white');
|
||||
ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
redrawTextElements();
|
||||
redrawLineSegments();
|
||||
convertDithering();
|
||||
|
||||
exitCropMode();
|
||||
saveToHistory(); // Save after finishing crop
|
||||
};
|
||||
image.src = URL.createObjectURL(imageFile.files[0]);
|
||||
}
|
||||
|
||||
function handleTouchStart(e) {
|
||||
e.preventDefault();
|
||||
if (e.touches.length === 1) {
|
||||
handleBackgroundPanStart(e.touches[0]);
|
||||
} else if (e.touches.length === 2) {
|
||||
isPanning = false; // Stop panning when zooming
|
||||
lastTouchDistance = getTouchDistance(e.touches);
|
||||
resetStates() {
|
||||
this.backgroundZoom = 1;
|
||||
this.backgroundPanX = 0;
|
||||
this.backgroundPanY = 0;
|
||||
this.isPanning = false;
|
||||
this.lastPanX = 0;
|
||||
this.lastPanY = 0;
|
||||
this.lastTouchDistance = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
e.preventDefault();
|
||||
if (isPanning && e.touches.length === 1) {
|
||||
handleBackgroundPan(e.touches[0]);
|
||||
} else if (e.touches.length === 2) {
|
||||
const newDist = getTouchDistance(e.touches);
|
||||
if (lastTouchDistance > 0) {
|
||||
const zoomFactor = newDist / lastTouchDistance;
|
||||
backgroundZoom *= zoomFactor;
|
||||
backgroundZoom = Math.max(0.1, Math.min(5, backgroundZoom)); // Limit zoom range
|
||||
updateBackgroundTransform();
|
||||
isCropMode() {
|
||||
return this.canvas.parentNode.classList.contains('crop-mode');
|
||||
}
|
||||
|
||||
exitCropMode() {
|
||||
this.canvas.parentNode.classList.remove('crop-mode');
|
||||
setCanvasTitle("");
|
||||
|
||||
this.canvas.removeEventListener('wheel', this.handleBackgroundZoom);
|
||||
this.canvas.removeEventListener('mousedown', this.handleBackgroundPanStart);
|
||||
this.canvas.removeEventListener('mousemove', this.handleBackgroundPan);
|
||||
this.canvas.removeEventListener('mouseup', this.handleBackgroundPanEnd);
|
||||
this.canvas.removeEventListener('mouseleave', this.handleBackgroundPanEnd);
|
||||
this.canvas.removeEventListener('touchstart', this.handleTouchStart);
|
||||
this.canvas.removeEventListener('touchmove', this.handleTouchMove);
|
||||
this.canvas.removeEventListener('touchend', this.handleBackgroundPanEnd);
|
||||
this.canvas.removeEventListener('touchcancel', this.handleBackgroundPanEnd);
|
||||
}
|
||||
|
||||
initializeCrop() {
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
if (imageFile.files.length == 0) {
|
||||
fillCanvas('white');
|
||||
return;
|
||||
}
|
||||
lastTouchDistance = newDist;
|
||||
|
||||
this.exitCropMode();
|
||||
this.resetStates();
|
||||
|
||||
this.canvas.style.backgroundImage = `url(${URL.createObjectURL(imageFile.files[0])})`;
|
||||
this.canvas.style.backgroundSize = '100%';
|
||||
this.canvas.style.backgroundPosition = '';
|
||||
this.canvas.style.backgroundRepeat = 'no-repeat';
|
||||
|
||||
// add event listeners for zoom and pan
|
||||
this.canvas.addEventListener('wheel', this.handleBackgroundZoom);
|
||||
this.canvas.addEventListener('mousedown', this.handleBackgroundPanStart);
|
||||
this.canvas.addEventListener('mousemove', this.handleBackgroundPan);
|
||||
this.canvas.addEventListener('mouseup', this.handleBackgroundPanEnd);
|
||||
this.canvas.addEventListener('mouseleave', this.handleBackgroundPanEnd);
|
||||
|
||||
// Touch events for mobile devices
|
||||
this.canvas.addEventListener('touchstart', this.handleTouchStart);
|
||||
this.canvas.addEventListener('touchmove', this.handleTouchMove);
|
||||
this.canvas.addEventListener('touchend', this.handleBackgroundPanEnd);
|
||||
this.canvas.addEventListener('touchcancel', this.handleBackgroundPanEnd);
|
||||
|
||||
// Make the canvas transparent
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
setCanvasTitle("裁剪模式: 可用鼠标滚轮或双指触摸缩放图片");
|
||||
this.canvas.parentNode.classList.add('crop-mode');
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackgroundZoom(e) {
|
||||
e.preventDefault();
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
backgroundZoom *= zoomFactor;
|
||||
backgroundZoom = Math.max(0.1, Math.min(5, backgroundZoom)); // Limit zoom range
|
||||
updateBackgroundTransform();
|
||||
}
|
||||
finishCrop() {
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
if (imageFile.files.length == 0) return;
|
||||
|
||||
function handleBackgroundPanStart(e) {
|
||||
isPanning = true;
|
||||
lastPanX = e.clientX;
|
||||
lastPanY = e.clientY;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
URL.revokeObjectURL(image.src);
|
||||
|
||||
function handleBackgroundPan(e) {
|
||||
if (isPanning) {
|
||||
const deltaX = e.clientX - lastPanX;
|
||||
const deltaY = e.clientY - lastPanY;
|
||||
backgroundPanX += deltaX;
|
||||
backgroundPanY += deltaY;
|
||||
lastPanX = e.clientX;
|
||||
lastPanY = e.clientY;
|
||||
updateBackgroundTransform();
|
||||
const fieldsetRect = this.canvas.getBoundingClientRect();
|
||||
const scale = (image.width / fieldsetRect.width) / this.backgroundZoom;
|
||||
|
||||
const sx = -this.backgroundPanX * scale;
|
||||
const sy = -this.backgroundPanY * scale;
|
||||
const sWidth = fieldsetRect.width * scale;
|
||||
const sHeight = fieldsetRect.height * scale;
|
||||
|
||||
fillCanvas('white');
|
||||
this.ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.paintManager.redrawTextElements();
|
||||
this.paintManager.redrawLineSegments();
|
||||
convertDithering();
|
||||
|
||||
this.exitCropMode();
|
||||
this.paintManager.saveToHistory(); // Save after finishing crop
|
||||
};
|
||||
image.src = URL.createObjectURL(imageFile.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackgroundPanEnd() {
|
||||
isPanning = false;
|
||||
lastTouchDistance = 0; // Reset touch distance
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
|
||||
function updateBackgroundTransform() {
|
||||
canvas.style.backgroundSize = `${100 * backgroundZoom}%`;
|
||||
canvas.style.backgroundPosition = `${backgroundPanX}px ${backgroundPanY}px`;
|
||||
}
|
||||
|
||||
function getTouchDistance(touches) {
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function initCropTools() {
|
||||
document.getElementById('crop-zoom-in').addEventListener('click', (e) => {
|
||||
handleTouchStart(e) {
|
||||
e.preventDefault();
|
||||
handleBackgroundZoom({ preventDefault: () => {}, deltaY: -1 });
|
||||
});
|
||||
if (e.touches.length === 1) {
|
||||
this.handleBackgroundPanStart(e.touches[0]);
|
||||
} else if (e.touches.length === 2) {
|
||||
this.isPanning = false; // Stop panning when zooming
|
||||
this.lastTouchDistance = this.getTouchDistance(e.touches);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('crop-zoom-out').addEventListener('click', (e) => {
|
||||
handleTouchMove(e) {
|
||||
e.preventDefault();
|
||||
handleBackgroundZoom({ preventDefault: () => {}, deltaY: 1 });
|
||||
});
|
||||
if (this.isPanning && e.touches.length === 1) {
|
||||
this.handleBackgroundPan(e.touches[0]);
|
||||
} else if (e.touches.length === 2) {
|
||||
const newDist = this.getTouchDistance(e.touches);
|
||||
if (this.lastTouchDistance > 0) {
|
||||
const zoomFactor = newDist / this.lastTouchDistance;
|
||||
this.backgroundZoom *= zoomFactor;
|
||||
this.backgroundZoom = Math.max(0.1, Math.min(5, this.backgroundZoom)); // Limit zoom range
|
||||
this.updateBackgroundTransform();
|
||||
}
|
||||
this.lastTouchDistance = newDist;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('crop-move-left').addEventListener('click', (e) => {
|
||||
handleBackgroundZoom(e) {
|
||||
e.preventDefault();
|
||||
backgroundPanX -= 10;
|
||||
updateBackgroundTransform();
|
||||
});
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
this.backgroundZoom *= zoomFactor;
|
||||
this.backgroundZoom = Math.max(0.1, Math.min(5, this.backgroundZoom)); // Limit zoom range
|
||||
this.updateBackgroundTransform();
|
||||
}
|
||||
|
||||
document.getElementById('crop-move-right').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
backgroundPanX += 10;
|
||||
updateBackgroundTransform();
|
||||
});
|
||||
handleBackgroundPanStart(e) {
|
||||
this.isPanning = true;
|
||||
this.lastPanX = e.clientX;
|
||||
this.lastPanY = e.clientY;
|
||||
this.canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
document.getElementById('crop-move-up').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
backgroundPanY -= 10;
|
||||
updateBackgroundTransform();
|
||||
});
|
||||
handleBackgroundPan(e) {
|
||||
if (this.isPanning) {
|
||||
const deltaX = e.clientX - this.lastPanX;
|
||||
const deltaY = e.clientY - this.lastPanY;
|
||||
this.backgroundPanX += deltaX;
|
||||
this.backgroundPanY += deltaY;
|
||||
this.lastPanX = e.clientX;
|
||||
this.lastPanY = e.clientY;
|
||||
this.updateBackgroundTransform();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('crop-move-down').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
backgroundPanY += 10;
|
||||
updateBackgroundTransform();
|
||||
});
|
||||
handleBackgroundPanEnd() {
|
||||
this.isPanning = false;
|
||||
this.lastTouchDistance = 0; // Reset touch distance
|
||||
this.canvas.style.cursor = 'grab';
|
||||
}
|
||||
|
||||
updateBackgroundTransform() {
|
||||
this.canvas.style.backgroundSize = `${100 * this.backgroundZoom}%`;
|
||||
this.canvas.style.backgroundPosition = `${this.backgroundPanX}px ${this.backgroundPanY}px`;
|
||||
}
|
||||
|
||||
getTouchDistance(touches) {
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
);
|
||||
}
|
||||
|
||||
initCropTools() {
|
||||
document.getElementById('crop-zoom-in').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleBackgroundZoom({ preventDefault: () => { }, deltaY: -1 });
|
||||
});
|
||||
|
||||
document.getElementById('crop-zoom-out').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleBackgroundZoom({ preventDefault: () => { }, deltaY: 1 });
|
||||
});
|
||||
|
||||
document.getElementById('crop-move-left').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.backgroundPanX -= 10;
|
||||
this.updateBackgroundTransform();
|
||||
});
|
||||
|
||||
document.getElementById('crop-move-right').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.backgroundPanX += 10;
|
||||
this.updateBackgroundTransform();
|
||||
});
|
||||
|
||||
document.getElementById('crop-move-up').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.backgroundPanY -= 10;
|
||||
this.updateBackgroundTransform();
|
||||
});
|
||||
|
||||
document.getElementById('crop-move-down').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.backgroundPanY += 10;
|
||||
this.updateBackgroundTransform();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -508,7 +508,7 @@ function bayerDither(imageData, strength, mode) {
|
||||
|
||||
// Find closest color in palette
|
||||
const closest = findClosestColor(clampedR, clampedG, clampedB, mode);
|
||||
|
||||
|
||||
data[idx] = closest.r;
|
||||
data[idx + 1] = closest.g;
|
||||
data[idx + 2] = closest.b;
|
||||
|
||||
@@ -2,24 +2,25 @@ let bleDevice, gattServer;
|
||||
let epdService, epdCharacteristic;
|
||||
let startTime, msgIndex, appVersion;
|
||||
let canvas, ctx, textDecoder;
|
||||
let paintManager, cropManager;
|
||||
|
||||
const EpdCmd = {
|
||||
SET_PINS: 0x00,
|
||||
INIT: 0x01,
|
||||
CLEAR: 0x02,
|
||||
SEND_CMD: 0x03,
|
||||
SET_PINS: 0x00,
|
||||
INIT: 0x01,
|
||||
CLEAR: 0x02,
|
||||
SEND_CMD: 0x03,
|
||||
SEND_DATA: 0x04,
|
||||
REFRESH: 0x05,
|
||||
SLEEP: 0x06,
|
||||
REFRESH: 0x05,
|
||||
SLEEP: 0x06,
|
||||
|
||||
SET_TIME: 0x20,
|
||||
SET_TIME: 0x20,
|
||||
|
||||
WRITE_IMG: 0x30, // v1.6
|
||||
|
||||
SET_CONFIG: 0x90,
|
||||
SYS_RESET: 0x91,
|
||||
SYS_SLEEP: 0x92,
|
||||
CFG_ERASE: 0x99,
|
||||
SYS_RESET: 0x91,
|
||||
SYS_SLEEP: 0x92,
|
||||
CFG_ERASE: 0x99,
|
||||
};
|
||||
|
||||
const canvasSizes = [
|
||||
@@ -190,7 +191,7 @@ function convertUC8159(blackWhiteData, redWhiteData) {
|
||||
}
|
||||
|
||||
async function sendimg() {
|
||||
if (isCropMode()) {
|
||||
if (cropManager.isCropMode()) {
|
||||
alert("请先完成图片裁剪!发送已取消。");
|
||||
return;
|
||||
}
|
||||
@@ -249,7 +250,7 @@ async function sendimg() {
|
||||
}
|
||||
|
||||
function downloadDataArray() {
|
||||
if (isCropMode()) {
|
||||
if (cropManager.isCropMode()) {
|
||||
alert("请先完成图片裁剪!下载已取消。");
|
||||
return;
|
||||
}
|
||||
@@ -440,6 +441,7 @@ function addLog(logTXT, action = '') {
|
||||
|
||||
const logEntry = document.createElement('div');
|
||||
const timeSpan = document.createElement('span');
|
||||
logEntry.className = 'log-line';
|
||||
timeSpan.className = 'time';
|
||||
timeSpan.textContent = time;
|
||||
logEntry.appendChild(timeSpan);
|
||||
@@ -469,6 +471,14 @@ function fillCanvas(style) {
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function setCanvasTitle(title) {
|
||||
const canvasTitle = document.querySelector('.canvas-title');
|
||||
if (canvasTitle) {
|
||||
canvasTitle.innerText = title;
|
||||
canvasTitle.style.display = title && title !== '' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateImage() {
|
||||
const imageFile = document.getElementById('imageFile');
|
||||
if (imageFile.files.length == 0) {
|
||||
@@ -480,16 +490,16 @@ function updateImage() {
|
||||
image.onload = function () {
|
||||
URL.revokeObjectURL(this.src);
|
||||
if (image.width / image.height == canvas.width / canvas.height) {
|
||||
if (isCropMode()) exitCropMode();
|
||||
if (cropManager.isCropMode()) cropManager.exitCropMode();
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
|
||||
redrawTextElements();
|
||||
redrawLineSegments();
|
||||
paintManager.redrawTextElements();
|
||||
paintManager.redrawLineSegments();
|
||||
convertDithering();
|
||||
saveToHistory(); // Save after loading image
|
||||
paintManager.saveToHistory(); // Save after loading image
|
||||
} else {
|
||||
alert("图片宽高比例与画布不匹配,将进入裁剪模式。\n请放大图片后移动图片使其充满画布,再点击“完成”按钮。");
|
||||
setActiveTool(null, '');
|
||||
initializeCrop();
|
||||
alert(`图片宽高比例与画布不匹配,将进入裁剪模式。\n请放大图片后移动图片使其充满画布, 再点击"完成"按钮。`);
|
||||
paintManager.setActiveTool(null, '');
|
||||
cropManager.initializeCrop();
|
||||
}
|
||||
};
|
||||
image.src = URL.createObjectURL(imageFile.files[0]);
|
||||
@@ -529,10 +539,9 @@ function rotateCanvas() {
|
||||
function clearCanvas() {
|
||||
if (confirm('清除画布内容?')) {
|
||||
fillCanvas('white');
|
||||
textElements = []; // Clear stored text positions
|
||||
lineSegments = []; // Clear stored line segments
|
||||
if (isCropMode()) exitCropMode();
|
||||
saveToHistory(); // Save cleared canvas to history
|
||||
paintManager.clearElements(); // Clear stored text positions and line segments
|
||||
if (cropManager.isCropMode()) cropManager.exitCropMode();
|
||||
paintManager.saveToHistory(); // Save cleared canvas to history
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -560,14 +569,14 @@ function convertDithering() {
|
||||
function initEventHandlers() {
|
||||
document.getElementById("epddriver").addEventListener("change", updateDitcherOptions);
|
||||
document.getElementById("imageFile").addEventListener("change", updateImage);
|
||||
document.getElementById("ditherMode").addEventListener("change", finishCrop);
|
||||
document.getElementById("ditherAlg").addEventListener("change", finishCrop);
|
||||
document.getElementById("ditherMode").addEventListener("change", () => cropManager.finishCrop());
|
||||
document.getElementById("ditherAlg").addEventListener("change", () => cropManager.finishCrop());
|
||||
document.getElementById("ditherStrength").addEventListener("input", function () {
|
||||
finishCrop();
|
||||
cropManager.finishCrop();
|
||||
document.getElementById("ditherStrengthValue").innerText = parseFloat(this.value).toFixed(1);
|
||||
});
|
||||
document.getElementById("ditherContrast").addEventListener("input", function () {
|
||||
finishCrop();
|
||||
cropManager.finishCrop();
|
||||
document.getElementById("ditherContrastValue").innerText = parseFloat(this.value).toFixed(1);
|
||||
});
|
||||
document.getElementById("canvasSize").addEventListener("change", updateCanvasSize);
|
||||
@@ -579,12 +588,12 @@ function checkDebugMode() {
|
||||
const debugMode = urlParams.get('debug');
|
||||
|
||||
if (debugMode === 'true') {
|
||||
document.body.classList.add('debug-mode');
|
||||
document.body.classList.add('dark-mode');
|
||||
link.innerHTML = '正常模式';
|
||||
link.setAttribute('href', window.location.pathname);
|
||||
addLog("注意:开发模式功能已开启!不懂请不要随意修改,否则后果自负!");
|
||||
} else {
|
||||
document.body.classList.remove('debug-mode');
|
||||
document.body.classList.remove('dark-mode');
|
||||
link.innerHTML = '开发模式';
|
||||
link.setAttribute('href', window.location.pathname + '?debug=true');
|
||||
}
|
||||
@@ -598,8 +607,11 @@ document.body.onload = () => {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
initPaintTools();
|
||||
initCropTools();
|
||||
paintManager = new PaintManager(canvas, ctx);
|
||||
cropManager = new CropManager(canvas, ctx, paintManager);
|
||||
|
||||
paintManager.initPaintTools();
|
||||
cropManager.initCropTools();
|
||||
initEventHandlers();
|
||||
updateButtonStatus();
|
||||
checkDebugMode();
|
||||
|
||||
913
html/js/paint.js
913
html/js/paint.js
@@ -1,502 +1,515 @@
|
||||
let painting = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
let brushColor = "#000000";
|
||||
let brushSize = 2;
|
||||
let currentTool = null; // Start with no tool selected
|
||||
let textElements = []; // Store text elements for re-rendering after dithering
|
||||
let lineSegments = []; // Store line segments for re-rendering after dithering
|
||||
let isTextPlacementMode = false;
|
||||
let draggingCanvasContext = null; // Backup of the canvas for dragging
|
||||
let selectedTextElement = null; // Track the currently selected text for dragging
|
||||
let isDraggingText = false; // Track if we're currently dragging text
|
||||
let dragOffsetX = 0; // Offset from mouse to text position when dragging
|
||||
let dragOffsetY = 0;
|
||||
let textBold = false; // Track if text should be bold
|
||||
let textItalic = false; // Track if text should be italic
|
||||
class PaintManager {
|
||||
constructor(canvas, ctx) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.painting = false;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.brushColor = "#000000";
|
||||
this.brushSize = 2;
|
||||
this.currentTool = null;
|
||||
this.textElements = [];
|
||||
this.lineSegments = [];
|
||||
this.isTextPlacementMode = false;
|
||||
this.draggingCanvasContext = null;
|
||||
this.selectedTextElement = null;
|
||||
this.isDraggingText = false;
|
||||
this.dragOffsetX = 0;
|
||||
this.dragOffsetY = 0;
|
||||
this.textBold = false;
|
||||
this.textItalic = false;
|
||||
|
||||
// Undo/Redo functionality
|
||||
let historyStack = []; // Stack to store canvas history
|
||||
let historyStep = -1; // Current position in history stack
|
||||
const MAX_HISTORY = 50; // Maximum number of undo steps
|
||||
// Undo/Redo functionality
|
||||
this.historyStack = [];
|
||||
this.historyStep = -1;
|
||||
this.MAX_HISTORY = 50;
|
||||
|
||||
function setCanvasTitle(title) {
|
||||
const canvasTitle = document.querySelector('.canvas-title');
|
||||
if (canvasTitle) {
|
||||
canvasTitle.innerText = title;
|
||||
canvasTitle.style.display = title && title !== '' ? 'block' : 'none';
|
||||
// Bind event handlers
|
||||
this.startPaint = this.startPaint.bind(this);
|
||||
this.paint = this.paint.bind(this);
|
||||
this.endPaint = this.endPaint.bind(this);
|
||||
this.handleCanvasClick = this.handleCanvasClick.bind(this);
|
||||
this.onTouchStart = this.onTouchStart.bind(this);
|
||||
this.onTouchMove = this.onTouchMove.bind(this);
|
||||
this.onTouchEnd = this.onTouchEnd.bind(this);
|
||||
this.handleKeyboard = this.handleKeyboard.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
function saveToHistory() {
|
||||
// Remove any states after current step (when user drew something after undoing)
|
||||
historyStack = historyStack.slice(0, historyStep + 1);
|
||||
|
||||
// Save current canvas state along with text and line data
|
||||
const canvasState = {
|
||||
imageData: ctx.getImageData(0, 0, canvas.width, canvas.height),
|
||||
textElements: JSON.parse(JSON.stringify(textElements)),
|
||||
lineSegments: JSON.parse(JSON.stringify(lineSegments))
|
||||
};
|
||||
|
||||
historyStack.push(canvasState);
|
||||
historyStep++;
|
||||
|
||||
// Limit history size
|
||||
if (historyStack.length > MAX_HISTORY) {
|
||||
historyStack.shift();
|
||||
historyStep--;
|
||||
}
|
||||
|
||||
updateUndoRedoButtons();
|
||||
}
|
||||
saveToHistory() {
|
||||
// Remove any states after current step (when user drew something after undoing)
|
||||
this.historyStack = this.historyStack.slice(0, this.historyStep + 1);
|
||||
|
||||
function undo() {
|
||||
if (historyStep > 0) {
|
||||
historyStep--;
|
||||
restoreFromHistory();
|
||||
}
|
||||
}
|
||||
// Save current canvas state along with text and line data
|
||||
const canvasState = {
|
||||
imageData: this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height),
|
||||
textElements: JSON.parse(JSON.stringify(this.textElements)),
|
||||
lineSegments: JSON.parse(JSON.stringify(this.lineSegments))
|
||||
};
|
||||
|
||||
function redo() {
|
||||
if (historyStep < historyStack.length - 1) {
|
||||
historyStep++;
|
||||
restoreFromHistory();
|
||||
}
|
||||
}
|
||||
this.historyStack.push(canvasState);
|
||||
this.historyStep++;
|
||||
|
||||
function restoreFromHistory() {
|
||||
if (historyStep >= 0 && historyStep < historyStack.length) {
|
||||
const state = historyStack[historyStep];
|
||||
|
||||
// Restore canvas image
|
||||
ctx.putImageData(state.imageData, 0, 0);
|
||||
|
||||
// Restore text and line data
|
||||
textElements = JSON.parse(JSON.stringify(state.textElements));
|
||||
lineSegments = JSON.parse(JSON.stringify(state.lineSegments));
|
||||
|
||||
updateUndoRedoButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateUndoRedoButtons() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
const redoBtn = document.getElementById('redo-btn');
|
||||
|
||||
if (undoBtn) {
|
||||
undoBtn.disabled = historyStep <= 0;
|
||||
}
|
||||
|
||||
if (redoBtn) {
|
||||
redoBtn.disabled = historyStep >= historyStack.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function initPaintTools() {
|
||||
document.getElementById('brush-mode').addEventListener('click', () => {
|
||||
if (currentTool === 'brush') {
|
||||
setActiveTool(null, '');
|
||||
} else {
|
||||
setActiveTool('brush', '画笔模式');
|
||||
brushColor = document.getElementById('brush-color').value;
|
||||
// Limit history size
|
||||
if (this.historyStack.length > this.MAX_HISTORY) {
|
||||
this.historyStack.shift();
|
||||
this.historyStep--;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('eraser-mode').addEventListener('click', () => {
|
||||
if (currentTool === 'eraser') {
|
||||
setActiveTool(null, '');
|
||||
} else {
|
||||
setActiveTool('eraser', '橡皮擦');
|
||||
brushColor = "#FFFFFF";
|
||||
|
||||
this.updateUndoRedoButtons();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.historyStep > 0) {
|
||||
this.historyStep--;
|
||||
this.restoreFromHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('text-mode').addEventListener('click', () => {
|
||||
if (currentTool === 'text') {
|
||||
setActiveTool(null, '');
|
||||
} else {
|
||||
setActiveTool('text', '插入文字');
|
||||
brushColor = document.getElementById('brush-color').value;
|
||||
redo() {
|
||||
if (this.historyStep < this.historyStack.length - 1) {
|
||||
this.historyStep++;
|
||||
this.restoreFromHistory();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('brush-color').addEventListener('change', (e) => {
|
||||
brushColor = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('brush-size').addEventListener('change', (e) => {
|
||||
brushSize = parseInt(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('add-text-btn').addEventListener('click', startTextPlacement);
|
||||
restoreFromHistory() {
|
||||
if (this.historyStep >= 0 && this.historyStep < this.historyStack.length) {
|
||||
const state = this.historyStack[this.historyStep];
|
||||
|
||||
// Add event listeners for bold and italic buttons
|
||||
document.getElementById('text-bold').addEventListener('click', () => {
|
||||
textBold = !textBold;
|
||||
document.getElementById('text-bold').classList.toggle('primary', textBold);
|
||||
});
|
||||
|
||||
document.getElementById('text-italic').addEventListener('click', () => {
|
||||
textItalic = !textItalic;
|
||||
document.getElementById('text-italic').classList.toggle('primary', textItalic);
|
||||
});
|
||||
// Restore canvas image
|
||||
this.ctx.putImageData(state.imageData, 0, 0);
|
||||
|
||||
// Add undo/redo button listeners
|
||||
document.getElementById('undo-btn').addEventListener('click', undo);
|
||||
document.getElementById('redo-btn').addEventListener('click', redo);
|
||||
|
||||
canvas.addEventListener('mousedown', startPaint);
|
||||
canvas.addEventListener('mousemove', paint);
|
||||
canvas.addEventListener('mouseup', endPaint);
|
||||
canvas.addEventListener('mouseleave', endPaint);
|
||||
canvas.addEventListener('click', handleCanvasClick);
|
||||
|
||||
// Touch support
|
||||
canvas.addEventListener('touchstart', onTouchStart);
|
||||
canvas.addEventListener('touchmove', onTouchMove);
|
||||
canvas.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
// Keyboard shortcuts for undo/redo
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Restore text and line data
|
||||
this.textElements = JSON.parse(JSON.stringify(state.textElements));
|
||||
this.lineSegments = JSON.parse(JSON.stringify(state.lineSegments));
|
||||
|
||||
this.updateUndoRedoButtons();
|
||||
}
|
||||
}
|
||||
|
||||
updateUndoRedoButtons() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
const redoBtn = document.getElementById('redo-btn');
|
||||
|
||||
if (undoBtn) {
|
||||
undoBtn.disabled = this.historyStep <= 0;
|
||||
}
|
||||
|
||||
if (redoBtn) {
|
||||
redoBtn.disabled = this.historyStep >= this.historyStack.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
initPaintTools() {
|
||||
document.getElementById('brush-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'brush') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('brush', '画笔模式');
|
||||
this.brushColor = document.getElementById('brush-color').value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('eraser-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'eraser') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('eraser', '橡皮擦');
|
||||
this.brushColor = "#FFFFFF";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('text-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'text') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('text', '插入文字');
|
||||
this.brushColor = document.getElementById('brush-color').value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('brush-color').addEventListener('change', (e) => {
|
||||
this.brushColor = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('brush-size').addEventListener('change', (e) => {
|
||||
this.brushSize = parseInt(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('add-text-btn').addEventListener('click', () => this.startTextPlacement());
|
||||
|
||||
// Add event listeners for bold and italic buttons
|
||||
document.getElementById('text-bold').addEventListener('click', () => {
|
||||
this.textBold = !this.textBold;
|
||||
document.getElementById('text-bold').classList.toggle('primary', this.textBold);
|
||||
});
|
||||
|
||||
document.getElementById('text-italic').addEventListener('click', () => {
|
||||
this.textItalic = !this.textItalic;
|
||||
document.getElementById('text-italic').classList.toggle('primary', this.textItalic);
|
||||
});
|
||||
|
||||
// Add undo/redo button listeners
|
||||
document.getElementById('undo-btn').addEventListener('click', () => this.undo());
|
||||
document.getElementById('redo-btn').addEventListener('click', () => this.redo());
|
||||
|
||||
this.canvas.addEventListener('mousedown', this.startPaint);
|
||||
this.canvas.addEventListener('mousemove', this.paint);
|
||||
this.canvas.addEventListener('mouseup', this.endPaint);
|
||||
this.canvas.addEventListener('mouseleave', this.endPaint);
|
||||
this.canvas.addEventListener('click', this.handleCanvasClick);
|
||||
|
||||
// Touch support
|
||||
this.canvas.addEventListener('touchstart', this.onTouchStart);
|
||||
this.canvas.addEventListener('touchmove', this.onTouchMove);
|
||||
this.canvas.addEventListener('touchend', this.onTouchEnd);
|
||||
|
||||
// Keyboard shortcuts for undo/redo
|
||||
document.addEventListener('keydown', this.handleKeyboard);
|
||||
|
||||
// Initialize history with blank canvas state
|
||||
this.saveToHistory();
|
||||
}
|
||||
|
||||
handleKeyboard(e) {
|
||||
// Ctrl+Z or Cmd+Z for undo
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
undo();
|
||||
this.undo();
|
||||
}
|
||||
// Ctrl+Y or Ctrl+Shift+Z or Cmd+Shift+Z for redo
|
||||
else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
|
||||
e.preventDefault();
|
||||
redo();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize history with blank canvas state
|
||||
saveToHistory();
|
||||
}
|
||||
|
||||
function setActiveTool(tool, title) {
|
||||
setCanvasTitle(title);
|
||||
currentTool = tool;
|
||||
|
||||
canvas.parentNode.classList.toggle('brush-mode', currentTool === 'brush');
|
||||
canvas.parentNode.classList.toggle('eraser-mode', currentTool === 'eraser');
|
||||
canvas.parentNode.classList.toggle('text-mode', currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-mode').classList.toggle('active', currentTool === 'brush');
|
||||
document.getElementById('eraser-mode').classList.toggle('active', currentTool === 'eraser');
|
||||
document.getElementById('text-mode').classList.toggle('active', currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-color').disabled = currentTool === 'eraser';
|
||||
document.getElementById('brush-size').disabled = currentTool === 'text';
|
||||
|
||||
document.getElementById('undo-btn').classList.toggle('hide', currentTool === null);
|
||||
document.getElementById('redo-btn').classList.toggle('hide', currentTool === null);
|
||||
|
||||
// Cancel any pending text placement
|
||||
cancelTextPlacement();
|
||||
}
|
||||
|
||||
function startPaint(e) {
|
||||
if (!currentTool) return;
|
||||
|
||||
if (currentTool === 'text') {
|
||||
// Check if we're clicking on a text element to drag
|
||||
const textElement = findTextElementAt(e);
|
||||
if (textElement && textElement === selectedTextElement) {
|
||||
isDraggingText = true;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Calculate offset for smooth dragging
|
||||
dragOffsetX = textElement.x - x;
|
||||
dragOffsetY = textElement.y - y;
|
||||
|
||||
return; // Don't start drawing
|
||||
}
|
||||
} else {
|
||||
painting = true;
|
||||
draw(e);
|
||||
}
|
||||
}
|
||||
|
||||
function endPaint() {
|
||||
if (painting || isDraggingText) {
|
||||
saveToHistory(); // Save state after drawing or dragging text
|
||||
}
|
||||
painting = false;
|
||||
isDraggingText = false;
|
||||
lastX = 0;
|
||||
lastY = 0;
|
||||
}
|
||||
|
||||
function paint(e) {
|
||||
if (!currentTool) return;
|
||||
|
||||
if (currentTool === 'text') {
|
||||
if (isDraggingText && selectedTextElement) {
|
||||
dragText(e);
|
||||
}
|
||||
} else {
|
||||
if (painting) {
|
||||
draw(e);
|
||||
this.redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = brushColor;
|
||||
ctx.lineWidth = brushSize;
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
if (lastX === 0 && lastY === 0) {
|
||||
// For the first point, just do a dot
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x+0.1, y+0.1);
|
||||
|
||||
// Store the dot for redrawing
|
||||
lineSegments.push({
|
||||
type: 'dot',
|
||||
x: x,
|
||||
y: y,
|
||||
color: brushColor,
|
||||
size: brushSize
|
||||
});
|
||||
} else {
|
||||
// Connect to the previous point
|
||||
ctx.moveTo(lastX, lastY);
|
||||
ctx.lineTo(x, y);
|
||||
|
||||
// Store the line segment for redrawing
|
||||
lineSegments.push({
|
||||
type: 'line',
|
||||
x1: lastX,
|
||||
y1: lastY,
|
||||
x2: x,
|
||||
y2: y,
|
||||
color: brushColor,
|
||||
size: brushSize
|
||||
});
|
||||
setActiveTool(tool, title) {
|
||||
setCanvasTitle(title);
|
||||
this.currentTool = tool;
|
||||
|
||||
this.canvas.parentNode.classList.toggle('brush-mode', this.currentTool === 'brush');
|
||||
this.canvas.parentNode.classList.toggle('eraser-mode', this.currentTool === 'eraser');
|
||||
this.canvas.parentNode.classList.toggle('text-mode', this.currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-mode').classList.toggle('active', this.currentTool === 'brush');
|
||||
document.getElementById('eraser-mode').classList.toggle('active', this.currentTool === 'eraser');
|
||||
document.getElementById('text-mode').classList.toggle('active', this.currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-color').disabled = this.currentTool === 'eraser';
|
||||
document.getElementById('brush-size').disabled = this.currentTool === 'text';
|
||||
|
||||
document.getElementById('undo-btn').classList.toggle('hide', this.currentTool === null);
|
||||
document.getElementById('redo-btn').classList.toggle('hide', this.currentTool === null);
|
||||
|
||||
// Cancel any pending text placement
|
||||
this.cancelTextPlacement();
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
|
||||
function handleCanvasClick(e) {
|
||||
if (currentTool === 'text' && isTextPlacementMode) {
|
||||
placeText(e);
|
||||
startPaint(e) {
|
||||
if (!this.currentTool) return;
|
||||
|
||||
if (this.currentTool === 'text') {
|
||||
// Check if we're clicking on a text element to drag
|
||||
const textElement = this.findTextElementAt(e);
|
||||
if (textElement && textElement === this.selectedTextElement) {
|
||||
this.isDraggingText = true;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Calculate offset for smooth dragging
|
||||
this.dragOffsetX = textElement.x - x;
|
||||
this.dragOffsetY = textElement.y - y;
|
||||
|
||||
return; // Don't start drawing
|
||||
}
|
||||
} else {
|
||||
this.painting = true;
|
||||
this.draw(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Improve touch handling for text placement
|
||||
function onTouchStart(e) {
|
||||
endPaint() {
|
||||
if (this.painting || this.isDraggingText) {
|
||||
this.saveToHistory(); // Save state after drawing or dragging text
|
||||
}
|
||||
this.painting = false;
|
||||
this.isDraggingText = false;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
}
|
||||
|
||||
paint(e) {
|
||||
if (!this.currentTool) return;
|
||||
|
||||
if (this.currentTool === 'text') {
|
||||
if (this.isDraggingText && this.selectedTextElement) {
|
||||
this.dragText(e);
|
||||
}
|
||||
} else {
|
||||
if (this.painting) {
|
||||
this.draw(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
this.ctx.lineJoin = 'round';
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.strokeStyle = this.brushColor;
|
||||
this.ctx.lineWidth = this.brushSize;
|
||||
|
||||
this.ctx.beginPath();
|
||||
|
||||
if (this.lastX === 0 && this.lastY === 0) {
|
||||
// For the first point, just do a dot
|
||||
this.ctx.moveTo(x, y);
|
||||
this.ctx.lineTo(x + 0.1, y + 0.1);
|
||||
|
||||
// Store the dot for redrawing
|
||||
this.lineSegments.push({
|
||||
type: 'dot',
|
||||
x: x,
|
||||
y: y,
|
||||
color: this.brushColor,
|
||||
size: this.brushSize
|
||||
});
|
||||
} else {
|
||||
// Connect to the previous point
|
||||
this.ctx.moveTo(this.lastX, this.lastY);
|
||||
this.ctx.lineTo(x, y);
|
||||
|
||||
// Store the line segment for redrawing
|
||||
this.lineSegments.push({
|
||||
type: 'line',
|
||||
x1: this.lastX,
|
||||
y1: this.lastY,
|
||||
x2: x,
|
||||
y2: y,
|
||||
color: this.brushColor,
|
||||
size: this.brushSize
|
||||
});
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
|
||||
this.lastX = x;
|
||||
this.lastY = y;
|
||||
}
|
||||
|
||||
handleCanvasClick(e) {
|
||||
if (this.currentTool === 'text' && this.isTextPlacementMode) {
|
||||
this.placeText(e);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchStart(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
|
||||
|
||||
// If in text placement mode, handle as a click
|
||||
if (currentTool === 'text' && isTextPlacementMode) {
|
||||
const mouseEvent = new MouseEvent('click', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise handle as normal drawing
|
||||
const mouseEvent = new MouseEvent('mousedown', {
|
||||
if (this.currentTool === 'text' && this.isTextPlacementMode) {
|
||||
const mouseEvent = new MouseEvent('click', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
});
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
// Otherwise handle as normal drawing
|
||||
const mouseEvent = new MouseEvent('mousedown', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
|
||||
onTouchMove(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const mouseEvent = new MouseEvent('mousemove', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
|
||||
function onTouchEnd(e) {
|
||||
onTouchEnd(e) {
|
||||
e.preventDefault();
|
||||
endPaint();
|
||||
}
|
||||
|
||||
function dragText(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Update text position with offset
|
||||
selectedTextElement.x = x + dragOffsetX;
|
||||
selectedTextElement.y = y + dragOffsetY;
|
||||
|
||||
// Redraw selected text element
|
||||
if (draggingCanvasContext) {
|
||||
ctx.putImageData(draggingCanvasContext, 0, 0);
|
||||
} else {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.endPaint();
|
||||
}
|
||||
ctx.font = selectedTextElement.font;
|
||||
ctx.fillStyle = selectedTextElement.color;
|
||||
ctx.fillText(selectedTextElement.text, selectedTextElement.x, selectedTextElement.y);
|
||||
}
|
||||
|
||||
function findTextElementAt(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Search through text elements in reverse order (top-most first)
|
||||
for (let i = textElements.length - 1; i >= 0; i--) {
|
||||
const text = textElements[i];
|
||||
|
||||
// Calculate text dimensions
|
||||
ctx.font = text.font;
|
||||
const textWidth = ctx.measureText(text.text).width;
|
||||
dragText(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Extract font size correctly from the font string
|
||||
// This handles "bold 14px Arial", "italic 14px Arial", "bold italic 14px Arial", etc.
|
||||
const fontSizeMatch = text.font.match(/(\d+)px/);
|
||||
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1]) : 14; // Default to 14 if not found
|
||||
const textHeight = fontSize * 1.2; // Approximate height
|
||||
|
||||
// Check if click is within text bounds (allowing for some margin)
|
||||
const margin = 5;
|
||||
if (x >= text.x - margin &&
|
||||
x <= text.x + textWidth + margin &&
|
||||
y >= text.y - textHeight + margin &&
|
||||
y <= text.y + margin) {
|
||||
return text;
|
||||
// Update text position with offset
|
||||
this.selectedTextElement.x = x + this.dragOffsetX;
|
||||
this.selectedTextElement.y = y + this.dragOffsetY;
|
||||
|
||||
// Redraw selected text element
|
||||
if (this.draggingCanvasContext) {
|
||||
this.ctx.putImageData(this.draggingCanvasContext, 0, 0);
|
||||
} else {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
this.ctx.font = this.selectedTextElement.font;
|
||||
this.ctx.fillStyle = this.selectedTextElement.color;
|
||||
this.ctx.fillText(this.selectedTextElement.text, this.selectedTextElement.x, this.selectedTextElement.y);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function startTextPlacement() {
|
||||
const text = document.getElementById('text-input').value.trim();
|
||||
if (!text) {
|
||||
findTextElementAt(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Search through text elements in reverse order (top-most first)
|
||||
for (let i = this.textElements.length - 1; i >= 0; i--) {
|
||||
const text = this.textElements[i];
|
||||
|
||||
// Calculate text dimensions
|
||||
this.ctx.font = text.font;
|
||||
const textWidth = this.ctx.measureText(text.text).width;
|
||||
|
||||
// Extract font size correctly from the font string
|
||||
const fontSizeMatch = text.font.match(/(\d+)px/);
|
||||
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1]) : 14;
|
||||
const textHeight = fontSize * 1.2; // Approximate height
|
||||
|
||||
// Check if click is within text bounds (allowing for some margin)
|
||||
const margin = 5;
|
||||
if (x >= text.x - margin &&
|
||||
x <= text.x + textWidth + margin &&
|
||||
y >= text.y - textHeight + margin &&
|
||||
y <= text.y + margin) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
startTextPlacement() {
|
||||
const text = document.getElementById('text-input').value.trim();
|
||||
if (!text) {
|
||||
alert('请输入文字内容');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTextPlacementMode = true;
|
||||
|
||||
// Add visual feedback
|
||||
setCanvasTitle('点击画布放置文字');
|
||||
this.canvas.classList.add('text-placement-mode');
|
||||
}
|
||||
|
||||
isTextPlacementMode = true;
|
||||
cancelTextPlacement() {
|
||||
this.isTextPlacementMode = false;
|
||||
this.canvas.classList.remove('text-placement-mode');
|
||||
|
||||
// Add visual feedback
|
||||
setCanvasTitle('点击画布放置文字');
|
||||
canvas.classList.add('text-placement-mode');
|
||||
}
|
||||
|
||||
function cancelTextPlacement() {
|
||||
isTextPlacementMode = false;
|
||||
canvas.classList.remove('text-placement-mode');
|
||||
|
||||
// reset dragging state
|
||||
isDraggingText = false;
|
||||
dragOffsetX = 0;
|
||||
dragOffsetY = 0;
|
||||
selectedTextElement = null;
|
||||
draggingCanvasContext = null;
|
||||
}
|
||||
|
||||
function placeText(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const text = document.getElementById('text-input').value;
|
||||
const fontFamily = document.getElementById('font-family').value;
|
||||
const fontSize = document.getElementById('font-size').value;
|
||||
|
||||
// Build font style string
|
||||
let fontStyle = '';
|
||||
if (textItalic) fontStyle += 'italic ';
|
||||
if (textBold) fontStyle += 'bold ';
|
||||
|
||||
// Create a new text element
|
||||
const newText = {
|
||||
text: text,
|
||||
x: x,
|
||||
y: y,
|
||||
font: `${fontStyle}${fontSize}px ${fontFamily}`,
|
||||
color: brushColor
|
||||
};
|
||||
|
||||
// Add to our list of text elements
|
||||
textElements.push(newText);
|
||||
|
||||
// Select this text element for immediate dragging
|
||||
selectedTextElement = newText;
|
||||
draggingCanvasContext = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw text on canvas
|
||||
ctx.font = newText.font;
|
||||
ctx.fillStyle = newText.color;
|
||||
ctx.fillText(newText.text, newText.x, newText.y);
|
||||
|
||||
// Save to history after placing text
|
||||
saveToHistory();
|
||||
|
||||
// Reset
|
||||
document.getElementById('text-input').value = '';
|
||||
isTextPlacementMode = false;
|
||||
canvas.classList.remove('text-placement-mode');
|
||||
setCanvasTitle('拖动新添加文字可调整位置');
|
||||
}
|
||||
|
||||
function redrawTextElements() {
|
||||
// Redraw all text elements after dithering
|
||||
textElements.forEach(item => {
|
||||
ctx.font = item.font;
|
||||
ctx.fillStyle = item.color;
|
||||
ctx.fillText(item.text, item.x, item.y);
|
||||
});
|
||||
}
|
||||
|
||||
function redrawLineSegments() {
|
||||
// Redraw all line segments after dithering
|
||||
lineSegments.forEach(segment => {
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = segment.color;
|
||||
ctx.lineWidth = segment.size;
|
||||
ctx.beginPath();
|
||||
|
||||
if (segment.type === 'dot') {
|
||||
ctx.moveTo(segment.x, segment.y);
|
||||
ctx.lineTo(segment.x+0.1, segment.y+0.1);
|
||||
} else {
|
||||
ctx.moveTo(segment.x1, segment.y1);
|
||||
ctx.lineTo(segment.x2, segment.y2);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
});
|
||||
// reset dragging state
|
||||
this.isDraggingText = false;
|
||||
this.dragOffsetX = 0;
|
||||
this.dragOffsetY = 0;
|
||||
this.selectedTextElement = null;
|
||||
this.draggingCanvasContext = null;
|
||||
}
|
||||
|
||||
placeText(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const text = document.getElementById('text-input').value;
|
||||
const fontFamily = document.getElementById('font-family').value;
|
||||
const fontSize = document.getElementById('font-size').value;
|
||||
|
||||
// Build font style string
|
||||
let fontStyle = '';
|
||||
if (this.textItalic) fontStyle += 'italic ';
|
||||
if (this.textBold) fontStyle += 'bold ';
|
||||
|
||||
// Create a new text element
|
||||
const newText = {
|
||||
text: text,
|
||||
x: x,
|
||||
y: y,
|
||||
font: `${fontStyle}${fontSize}px ${fontFamily}`,
|
||||
color: this.brushColor
|
||||
};
|
||||
|
||||
// Add to our list of text elements
|
||||
this.textElements.push(newText);
|
||||
|
||||
// Select this text element for immediate dragging
|
||||
this.selectedTextElement = newText;
|
||||
this.draggingCanvasContext = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw text on canvas
|
||||
this.ctx.font = newText.font;
|
||||
this.ctx.fillStyle = newText.color;
|
||||
this.ctx.fillText(newText.text, newText.x, newText.y);
|
||||
|
||||
// Save to history after placing text
|
||||
this.saveToHistory();
|
||||
|
||||
// Reset
|
||||
document.getElementById('text-input').value = '';
|
||||
this.isTextPlacementMode = false;
|
||||
this.canvas.classList.remove('text-placement-mode');
|
||||
setCanvasTitle('拖动新添加文字可调整位置');
|
||||
}
|
||||
|
||||
redrawTextElements() {
|
||||
// Redraw all text elements after dithering
|
||||
this.textElements.forEach(item => {
|
||||
this.ctx.font = item.font;
|
||||
this.ctx.fillStyle = item.color;
|
||||
this.ctx.fillText(item.text, item.x, item.y);
|
||||
});
|
||||
}
|
||||
|
||||
redrawLineSegments() {
|
||||
// Redraw all line segments after dithering
|
||||
this.lineSegments.forEach(segment => {
|
||||
this.ctx.lineJoin = 'round';
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.strokeStyle = segment.color;
|
||||
this.ctx.lineWidth = segment.size;
|
||||
this.ctx.beginPath();
|
||||
|
||||
if (segment.type === 'dot') {
|
||||
this.ctx.moveTo(segment.x, segment.y);
|
||||
this.ctx.lineTo(segment.x + 0.1, segment.y + 0.1);
|
||||
} else {
|
||||
this.ctx.moveTo(segment.x1, segment.y1);
|
||||
this.ctx.lineTo(segment.x2, segment.y2);
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
clearElements() {
|
||||
this.textElements = [];
|
||||
this.lineSegments = [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user