一、AI生成玩具
大概的提示词:帮我生成一个炫彩的名言桌面壁纸生成器,支持从这些api里获取,支持多渠道,支持列表模式和壁纸模式,支持点击按钮保存图片。带上想要用的api,从里面拷贝
二、可玩性
- 挂cf变成在线,nginx等
- mac 使用Plash 变成桌面壁纸,保存为index.html,add website → local
三、代码
展开
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多渠道名言壁纸生成器</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #000;
font-family: 'Microsoft YaHei', Arial, sans-serif;
color: white;
overflow: hidden;
}
#canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
flex-wrap: wrap;
gap: 10px;
transition: opacity 0.3s ease;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
max-width: 90%;
justify-content: center;
}
button {
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
background: #4CAF50;
color: white;
cursor: pointer;
transition: 0.3s;
}
button:hover {
background: #45a049;
}
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
display: none;
z-index: 100;
}
#quoteCount {
width: 60px;
padding: 10px;
border-radius: 5px;
border: none;
background: #f1f1f1;
color: #333;
text-align: center;
}
#quoteCount::-webkit-inner-spin-button,
#quoteCount::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
#toggleControls {
position: fixed;
right: 20px;
top: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(76, 175, 80, 0.8);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
z-index: 100;
transition: 0.3s;
}
#toggleControls:hover {
background: rgba(76, 175, 80, 1);
}
#controls.hidden {
opacity: 0;
pointer-events: none;
}
.source-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
width: 100%;
}
.source-checkbox {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
transition: 0.3s;
}
.source-checkbox:hover {
background: rgba(255, 255, 255, 0.2);
}
.source-checkbox input {
margin-right: 5px;
}
#quotesContainer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
padding: 20px;
box-sizing: border-box;
max-height: 100vh;
overflow-y: auto;
display: none;
z-index: 5;
}
.quote-card {
width: 100%;
max-width: 800px;
margin-bottom: 15px;
padding: 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
transition: 0.3s;
position: relative;
}
.quote-card:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
cursor: pointer;
}
.quote-content {
font-size: 18px;
line-height: 1.5;
margin-bottom: 10px;
}
.quote-author {
font-size: 14px;
text-align: right;
font-style: italic;
color: rgba(255, 255, 255, 0.7);
}
.quote-source {
position: absolute;
top: 10px;
right: 10px;
background: rgba(76, 175, 80, 0.7);
padding: 3px 8px;
border-radius: 20px;
font-size: 12px;
}
.view-toggle {
margin: 10px 0;
display: flex;
gap: 10px;
}
.selected {
background: #1E88E5;
}
.selected:hover {
background: #1976D2;
}
#fontSizeControl {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
#fontSize {
width: 100px;
}
/* 滚动条样式 */
#quotesContainer::-webkit-scrollbar {
width: 8px;
}
#quotesContainer::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
#quotesContainer::-webkit-scrollbar-thumb {
background-color: rgba(76, 175, 80, 0.7);
border-radius: 4px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="quotesContainer"></div>
<div class="loading">加载中...</div>
<button id="toggleControls">⚙️</button>
<div id="controls">
<div class="source-options">
<label class="source-checkbox">
<input type="checkbox" name="source" value="one" checked> ONE一个
</label>
<label class="source-checkbox">
<input type="checkbox" name="source" value="iciba" checked> 金山词霸
</label>
<label class="source-checkbox">
<input type="checkbox" name="source" value="shanbay" checked> 扇贝单词
</label>
<label class="source-checkbox">
<input type="checkbox" name="source" value="hitokoto" checked> 一言
</label>
<label class="source-checkbox">
<input type="checkbox" name="source" value="jinrishici" checked> 今日诗词
</label>
</div>
<div id="fontSizeControl">
<span>字体大小:</span>
<input type="range" id="fontSize" min="20" max="60" value="36">
<span id="fontSizeValue">36px</span>
</div>
<div>
<span>每个来源获取:</span>
<input type="number" id="quoteCount" min="1" max="20" value="3" title="每个来源获取名言数量">
<span>条</span>
</div>
<div class="view-toggle">
<button id="btnWallpaper" class="selected" onclick="toggleView('wallpaper')">壁纸模式</button>
<button id="btnList" onclick="toggleView('list')">列表模式</button>
</div>
<div>
<button onclick="fetchAllQuotes()">获取名言</button>
<button onclick="generateNew()">随机壁纸</button>
<button onclick="saveImage()">保存图片</button>
</div>
</div>
<script>
// 配置API
const APIs = {
one: {
url: 'http://v3.wufazhuce.com:8000/api/channel/one/0/0', // 替换为备用API,因原API可能存在跨域问题
transform: data => {
return {
content: data.data.content_list[0].forward,
author: data.from || 'ONE一个',
source: 'one'
};
}
},
iciba: {
// CORS error,地址加:https://cors-anywhere.herokuapp.com/ 可以解决
url: 'https://cors-anywhere.herokuapp.com/https://open.iciba.com/dsapi/', // 替换为备用API,因原API可能存在跨域问题
transform: data => {
return {
content: data.note,
author: data.caption || '金山词霸每日一句',
source: 'iciba'
};
}
},
shanbay: {
url: 'https://cors-anywhere.herokuapp.com/https://apiv3.shanbay.com/weapps/dailyquote/quote', // 替换为备用API,因原API可能存在跨域问题
transform: data => {
return {
content: data.translation,
author: data.author || '扇贝单词',
source: 'shanbay'
};
}
},
hitokoto: {
url: 'https://v1.hitokoto.cn/?c=a',
transform: data => {
return {
content: data.hitokoto,
author: data.from,
source: 'hitokoto'
};
}
},
jinrishici: {
url: 'https://v1.jinrishici.com/all.json',
transform: data => {
return {
content: data.content,
author: `${data.origin ? data.origin : ''} · ${data.author ? data.author : ''} · ${data.category ? data.category : ''}`,
source: 'jinrishici'
};
}
}
};
const CONFIG = {
cacheKey: 'wallpaper_quotes_cache',
cacheDuration: 60 * 60 * 1000, // 1小时
retryTimes: 3,
retryDelay: 1000,
defaultQuoteCount: 3,
sourceLabels: {
one: 'ONE一个',
iciba: '金山词霸',
shanbay: '扇贝单词',
hitokoto: '一言',
jinrishici: '今日诗词'
},
sourceColors: {
one: '#FF5722',
iciba: '#2196F3',
shanbay: '#9C27B0',
hitokoto: '#FF9800',
jinrishici: '#4CAF50'
}
};
class QuotesManager {
static async fetchWithRetry(url, times = CONFIG.retryTimes) {
for (let i = 0; i < times; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
if (i === times - 1) throw error;
await new Promise(resolve => setTimeout(resolve, CONFIG.retryDelay));
}
}
}
static async fetchQuote(source) {
try {
const api = APIs[source];
if (!api) throw new Error(`Invalid source: ${source}`);
const data = await this.fetchWithRetry(api.url);
return api.transform(data);
} catch (error) {
console.error(`Error fetching quote from ${source}:`, error);
throw error;
}
}
static async fetchQuotes(source, count) {
const quotes = [];
const uniqueContents = new Set();
// 为了防止无限循环,设置最大尝试次数
let attempts = 0;
const maxAttempts = count * 3;
while (quotes.length < count && attempts < maxAttempts) {
try {
const quote = await this.fetchQuote(source);
// 去重逻辑
if (!uniqueContents.has(quote.content)) {
uniqueContents.add(quote.content);
quotes.push(quote);
}
} catch (error) {
console.error(`Error fetching quote ${quotes.length + 1} from ${source}:`, error);
}
attempts++;
}
return quotes;
}
static async fetchAllSelectedQuotes() {
const quotesContainer = document.getElementById('quotesContainer');
quotesContainer.innerHTML = '';
const loading = document.querySelector('.loading');
loading.style.display = 'block';
const selectedSources = Array.from(document.querySelectorAll('input[name="source"]:checked'))
.map(cb => cb.value);
if (selectedSources.length === 0) {
alert('请至少选择一个名言来源');
loading.style.display = 'none';
return [];
}
const count = parseInt(document.getElementById('quoteCount').value) || CONFIG.defaultQuoteCount;
const allQuotes = [];
for (const source of selectedSources) {
try {
const quotes = await this.fetchQuotes(source, count);
allQuotes.push(...quotes);
// 即时更新到列表中
quotes.forEach(quote => {
this.addQuoteToList(quote);
});
} catch (error) {
console.error(`Failed to fetch quotes from ${source}:`, error);
}
}
loading.style.display = 'none';
// 保存到缓存
localStorage.setItem(CONFIG.cacheKey, JSON.stringify({
quotes: allQuotes,
timestamp: Date.now()
}));
return allQuotes;
}
static addQuoteToList(quote) {
const quotesContainer = document.getElementById('quotesContainer');
const quoteCard = document.createElement('div');
quoteCard.className = 'quote-card';
const sourceTag = document.createElement('div');
sourceTag.className = 'quote-source';
sourceTag.textContent = CONFIG.sourceLabels[quote.source] || quote.source;
sourceTag.style.backgroundColor = CONFIG.sourceColors[quote.source] || '#4CAF50';
const content = document.createElement('div');
content.className = 'quote-content';
content.textContent = quote.content;
const author = document.createElement('div');
author.className = 'quote-author';
author.textContent = quote.author ? `—— ${quote.author}` : '';
quoteCard.appendChild(sourceTag);
quoteCard.appendChild(content);
quoteCard.appendChild(author);
// 点击卡片使用这个名言生成壁纸
quoteCard.addEventListener('click', () => {
drawQuote(quote);
toggleView('wallpaper');
});
quotesContainer.appendChild(quoteCard);
}
static getRandomQuote() {
try {
const cached = localStorage.getItem(CONFIG.cacheKey);
if (cached) {
const {quotes} = JSON.parse(cached);
if (quotes && quotes.length > 0) {
return quotes[Math.floor(Math.random() * quotes.length)];
}
}
return null;
} catch (error) {
console.error('Error getting random quote:', error);
return null;
}
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function generateGradient() {
// 生成渐变背景
const hue1 = Math.random() * 360;
const hue2 = (hue1 + 30 + Math.random() * 60) % 360; // 相近但不同的色调
const colors = [
`hsl(${hue1}, 70%, 60%)`,
`hsl(${hue2}, 70%, 60%)`
];
// 随机决定渐变方向
let gradient;
const direction = Math.floor(Math.random() * 4);
switch (direction) {
case 0: // 从上到下
gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
break;
case 1: // 从左到右
gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
break;
case 2: // 从左上到右下
gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
break;
case 3: // 从右上到左下
gradient = ctx.createLinearGradient(canvas.width, 0, 0, canvas.height);
break;
}
colors.forEach((color, i) => {
gradient.addColorStop(i / (colors.length - 1), color);
});
return gradient;
}
function drawQuote(quote) {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景
ctx.fillStyle = generateGradient();
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 添加纹理效果
ctx.globalAlpha = 0.05;
for (let i = 0; i < 5; i++) {
ctx.beginPath();
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
Math.random() * 200 + 50,
0,
Math.PI * 2
);
ctx.fillStyle = "#ffffff";
ctx.fill();
}
ctx.globalAlpha = 1;
// 获取字体大小 - 根据屏幕大小调整
const baseFontSize = parseInt(document.getElementById('fontSize').value) || 36;
const fontSize = Math.min(baseFontSize, window.innerWidth / 20); // 调整字体大小以适应屏幕
// 处理多行文本
const sourceLabel = CONFIG.sourceLabels[quote.source] || quote.source;
const lines = quote.content.split('\n');
const lineHeight = fontSize * 1.5;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 绘制小标签
ctx.font = `bold ${fontSize * 0.6}px 'Microsoft YaHei'`;
ctx.fillStyle = CONFIG.sourceColors[quote.source] || '#4CAF50';
// 创建一个圆角矩形背景
const tagText = sourceLabel;
const tagWidth = ctx.measureText(tagText).width + 20;
const tagHeight = fontSize * 0.8;
const tagX = canvas.width / 2 - tagWidth / 2;
const tagY = canvas.height / 4 - tagHeight / 2;
roundRect(ctx, tagX, tagY, tagWidth, tagHeight, 15);
ctx.fill();
// 绘制标签文字
ctx.fillStyle = 'white';
ctx.fillText(tagText, canvas.width / 2, canvas.height / 4);
// 绘制名言内容 - 处理长文本
ctx.font = `bold ${fontSize}px 'Microsoft YaHei'`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
// 处理长句子自动换行
const processedLines = [];
const maxWidth = canvas.width * 0.8; // 文本最大宽度为画布宽度的80%
lines.forEach(line => {
if (ctx.measureText(line).width <= maxWidth) {
processedLines.push(line);
} else {
// 分词处理长句子
let currentLine = '';
const words = line.split('');
for (let i = 0; i < words.length; i++) {
const testLine = currentLine + words[i];
if (ctx.measureText(testLine).width > maxWidth) {
processedLines.push(currentLine);
currentLine = words[i];
} else {
currentLine = testLine;
}
}
if (currentLine.length > 0) {
processedLines.push(currentLine);
}
}
});
const startY = canvas.height / 2 - (processedLines.length - 1 + (quote.author ? 1 : 0)) * lineHeight / 2;
processedLines.forEach((line, index) => {
ctx.fillText(line, canvas.width / 2, startY + index * lineHeight);
});
// 绘制作者
if (quote.author) {
ctx.font = `italic ${fontSize * 0.7}px 'Microsoft YaHei'`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillText(`—— ${quote.author}`, canvas.width / 2, startY + processedLines.length * lineHeight);
}
}
// 辅助函数:绘制圆角矩形
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
}
async function fetchAllQuotes() {
await QuotesManager.fetchAllSelectedQuotes();
if (document.getElementById('btnList').classList.contains('selected')) {
document.getElementById('quotesContainer').style.display = 'flex';
}
}
async function generateNew() {
const loading = document.querySelector('.loading');
loading.style.display = 'block';
try {
let quote = QuotesManager.getRandomQuote();
if (!quote) {
// 如果没有缓存的名言,尝试获取新的
const quotes = await QuotesManager.fetchAllSelectedQuotes();
if (quotes.length > 0) {
quote = quotes[Math.floor(Math.random() * quotes.length)];
} else {
throw new Error('没有可用的名言');
}
}
drawQuote(quote);
toggleView('wallpaper');
} catch (error) {
console.error('Error generating wallpaper:', error);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '20px Microsoft YaHei';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("获取名言失败,请稍后重试", canvas.width / 2, canvas.height / 2);
} finally {
loading.style.display = 'none';
}
}
function saveImage() {
const link = document.createElement('a');
link.download = '名言壁纸.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
function toggleView(view) {
const wallpaperBtn = document.getElementById('btnWallpaper');
const listBtn = document.getElementById('btnList');
const quotesContainer = document.getElementById('quotesContainer');
const canvas = document.getElementById('canvas');
if (view === 'wallpaper') {
wallpaperBtn.classList.add('selected');
listBtn.classList.remove('selected');
quotesContainer.style.display = 'none';
canvas.style.display = 'block';
} else {
wallpaperBtn.classList.remove('selected');
listBtn.classList.add('selected');
quotesContainer.style.display = 'flex';
canvas.style.display = 'none';
}
}
document.getElementById('toggleControls').addEventListener('click', () => {
const controls = document.getElementById('controls');
controls.classList.toggle('hidden');
// 保存状态到 localStorage
localStorage.setItem('controlsHidden', controls.classList.contains('hidden'));
});
document.getElementById('fontSize').addEventListener('input', function() {
document.getElementById('fontSizeValue').textContent = this.value + 'px';
// 如果当前有显示的名言,重新绘制
const quote = QuotesManager.getRandomQuote();
if (quote) {
drawQuote(quote);
}
});
window.addEventListener('resize', () => {
resizeCanvas();
const quote = QuotesManager.getRandomQuote();
if (quote) {
drawQuote(quote);
} else {
generateNew();
}
});
window.addEventListener('load', async () => {
resizeCanvas();
// 恢复控制面板状态
const controls = document.getElementById('controls');
const isHidden = localStorage.getItem('controlsHidden') === 'true';
if (isHidden) {
controls.classList.add('hidden');
}
// 尝试从缓存加载名言
const cached = localStorage.getItem(CONFIG.cacheKey);
if (cached) {
try {
const {quotes, timestamp} = JSON.parse(cached);
if (Date.now() - timestamp < CONFIG.cacheDuration && quotes.length > 0) {
// 显示缓存的引言
quotes.forEach(quote => {
QuotesManager.addQuoteToList(quote);
});
// 随机选择一个显示到壁纸
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
drawQuote(randomQuote);
return;
}
} catch (error) {
console.error('Error loading from cache:', error);
}
}
// 如果没有缓存或缓存已过期,获取新的名言
await generateNew();
});
</script>
</body>
</html>
四、效果
五、FAQ
代码不是最完美的可能还有bug,大佬别喷,哈哈哈。
Q1: 金山、扇贝等会跨域
CORS error,地址加:https://cors-anywhere.herokuapp.com/ 可以解决
Q2: 默认生成没有全屏
AI已优化解决。