Skip to content

Commit

Permalink
tmp
Browse files Browse the repository at this point in the history
  • Loading branch information
hkint committed Nov 9, 2024
1 parent 3ab118e commit cf4dfdf
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 64 deletions.
110 changes: 107 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,113 @@ import { Toaster } from 'sonner';

const inter = Inter({ subsets: ['latin'] });

// Define common metadata values
const siteName = 'Image Watermark';
const baseUrl = 'https://watermark.okhk.net';
const defaultImage = `https://okhk.net/api/og?title=${siteName}`;

export const metadata: Metadata = {
title: 'Image Watermark - Add Text Watermarks to Images',
description: 'A simple tool to add text watermarks to your image',
metadataBase: new URL(baseUrl),

// Basic Metadata
title: {
default: 'Offline Image Watermark - Add Text Watermarks Locally Without Upload',
template: '%s | Offline Image Watermark'
},
description: 'Free offline watermark tool for images - Add text watermarks locally without uploading. Your photos never leave your device. Adjust opacity, position, and font styles with complete privacy. No upload, no registration required.',

// Keywords - Updated to focus on offline/privacy aspects
keywords: [
'offline watermark',
'local image watermark',
'private watermark tool',
'no upload watermark',
'browser watermark',
'secure image watermark',
'private photo watermark',
'local photo processing',
'client-side watermark',
'watermark without upload',
'private image tool',
'offline photo editor'
],

// Canonical URL
alternates: {
canonical: '/',
},

// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},

// Open Graph - Updated to emphasize offline/privacy features
openGraph: {
type: 'website',
siteName: siteName,
title: 'Offline Image Watermark - Add Watermarks Privately Without Uploading',
description: 'Add watermarks to your photos privately in your browser - No upload needed. Your images stay on your device. Free, secure, and instant watermarking with complete privacy.',
images: [{
url: defaultImage,
width: 1200,
height: 630,
alt: 'Offline Image Watermark Tool - Private & Secure'
}],
locale: 'en_US',
url: baseUrl,
},

// Twitter Card - Updated for privacy focus
twitter: {
card: 'summary_large_image',
title: 'Offline Image Watermark - Private Watermarking Without Upload',
description: 'Add watermarks to your photos privately in your browser. No server upload, complete privacy, instant results. Free offline watermarking tool.',
images: [defaultImage],
creator: '@okhknet',
site: '@okhknet',
},

// Additional Metadata - Updated categories and tags
category: 'Privacy-focused Image Tools',
creator: siteName,
publisher: siteName,
applicationName: 'Offline Image Watermark',
formatDetection: {
telephone: false,
date: false,
address: false,
email: false,
},

// Icons
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.svg',
apple: '/apple-touch-icon.svg',
other: [
{
rel: 'icon',
type: 'image/svg+xml',
sizes: '32x32',
url: '/favicon-32x32.svg',
},
{
rel: 'icon',
type: 'image/svg+xml',
sizes: '16x16',
url: '/favicon-16x16.svg',
},
],
},
};

export default function RootLayout({
Expand All @@ -23,4 +127,4 @@ export default function RootLayout({
</body>
</html>
);
}
}
16 changes: 14 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,21 @@ export default function Home() {

const addWatermark = (x: number, y: number, rotation: number) => {
ctx.save();
ctx.translate(x, y);
// ctx.translate(x, y);
// ctx.rotate((rotation * Math.PI) / 180);
// ctx.fillText(watermark, 0, 0);

// 获取文本的宽度和高度
const textWidth = ctx.measureText(watermark).width;
const textHeight = parseInt(ctx.font, 10); // TODO 假设字体的高度等于字体大小,后续优化

// 将原点移动到文字中心
ctx.translate(x + textWidth / 2, y + textHeight / 2);
ctx.rotate((rotation * Math.PI) / 180);
ctx.fillText(watermark, 0, 0);

// 绘制文字,位置为 (-textWidth/2, -textHeight/2) 以中心为基准
ctx.fillText(watermark, -textWidth / 2, textHeight / 2);

ctx.restore();
};

Expand Down
142 changes: 112 additions & 30 deletions components/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
'use client';

import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, ChangeEvent } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ChevronDown, Palette } from 'lucide-react';

// Predefined watermark colors with preview and descriptions
const COLOR_PRESETS = [
{ value: '#A9A9A9', label: 'Light Gray', description: 'Soft gray for unobtrusive text' },
{ value: '#7D7D7D', label: 'Medium Gray', description: 'Neutral gray for balanced appearance' },
{ value: '#4B4B4B', label: 'Deep Gray', description: 'Dark gray for subtle visibility' },
{ value: '#1E90FF', label: 'Dodger Blue', description: 'Bright blue for clear visibility' },
{ value: '#FF6347', label: 'Tomato Red', description: 'Vibrant red for attention-grabbing' },
{ value: '#32CD32', label: 'Lime Green', description: 'Bright green for fresh look' },
{ value: '#9370DB', label: 'Medium Purple', description: 'Moderate purple for creative documents' },
{ value: '#FFD700', label: 'Gold', description: 'Elegant gold for premium feel' },
] as const;


interface ColorPickerProps {
value: string;
Expand All @@ -10,16 +23,29 @@ interface ColorPickerProps {

export function ColorPicker({ value, onChange }: ColorPickerProps) {
const [hexValue, setHexValue] = useState(value);
const [isOpen, setIsOpen] = useState(false);
const colorInputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setHexValue(value);
}, [value]);

const handleHexChange = (e: React.ChangeEvent<HTMLInputElement>) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const handleHexChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setHexValue(newValue);
if (/^#[0-9A-Fa-f]{6}$/.test(newValue)) {
if (/^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(newValue)) {
onChange(newValue);
}
};
Expand All @@ -30,32 +56,88 @@ export function ColorPicker({ value, onChange }: ColorPickerProps) {
}
};

const handlePresetSelect = (colorValue: string) => {

onChange(colorValue);
setHexValue(colorValue);
setIsOpen(false);
};

return (
<div className="grid grid-cols-2 gap-4">
<div className="relative">
<input
type="color"
value={value}
onChange={(e) => {
onChange(e.target.value);
setHexValue(e.target.value);
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
ref={colorInputRef}
/>
<div
className="h-12 w-full rounded-lg border border-gray-200 hover:border-gray-300 transition-colors duration-200 cursor-pointer shadow-sm"
style={{ backgroundColor: value }}
onClick={handleColorClick}
/>
<div className="space-y-3" ref={containerRef}>
<div className="grid grid-cols-2 gap-4">
<div className="relative">
<input
type="color"
value={value.slice(0, 7)}
onChange={(e) => {
onChange(e.target.value);
setHexValue(e.target.value);
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
ref={colorInputRef}
/>
<div
className="h-12 w-full rounded-lg border border-gray-200 hover:border-gray-300 transition-colors duration-200 cursor-pointer shadow-sm"
style={{ backgroundColor: value }}
onClick={handleColorClick}
/>
</div>

<div className="relative">
<div className="relative flex items-center">
<Input
value={hexValue}
onChange={handleHexChange}
placeholder="#000000"
className="h-12 pr-10 font-mono text-base"
maxLength={9}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 h-full hover:bg-transparent"
onClick={() => setIsOpen(!isOpen)}
>
<ChevronDown
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
</Button>
</div>

{isOpen && (
<div className="absolute w-full mt-1 bg-popover border rounded-md shadow-md z-50">
<div className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground border-b">
<Palette size={14} />
<span>Watermark Colors</span>
</div>
<div className="py-1 max-h-64 overflow-y-auto">
{COLOR_PRESETS.map((preset) => (
<button
key={preset.value}
className="w-full px-3 py-2 text-sm text-left hover:bg-accent focus:bg-accent focus:outline-none transition-colors"
onClick={() => handlePresetSelect(preset.value)}
>
<div className="flex items-center gap-2">
<div
className="w-6 h-6 rounded border"
style={{ backgroundColor: preset.value }}
/>
<div>
<div className="font-medium">{preset.label}</div>
<div className="text-muted-foreground text-xs mt-0.5 text-wrap">
{preset.description}
</div>
</div>
</div>
</button>
))}
</div>
</div>
)}
</div>
</div>
<Input
value={hexValue}
onChange={handleHexChange}
placeholder="#000000"
className="h-12 font-mono text-lg border-gray-200 focus:border-blue-300 transition-colors duration-200"
maxLength={7}
/>
</div>
);
}
}
2 changes: 1 addition & 1 deletion components/ImagePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function ImagePreview({ processedImage }: ImagePreviewProps) {

return (
<div className="max-w-5xl mx-auto space-y-6">
<div className="relative aspect-video overflow-hidden rounded-sm border border-dashed border-blue-100 hover:border-blue-200 transition-colors duration-200 shadow">
<div className="relative aspect-video overflow-hidden rounded-2xl border border-dashed border-blue-100 hover:border-blue-200 transition-colors duration-200 shadow">
{processedImage ? (
<img
src={processedImage}
Expand Down
Loading

0 comments on commit cf4dfdf

Please sign in to comment.