552 lines
19 KiB
JavaScript
552 lines
19 KiB
JavaScript
// Doodleloop App Logic
|
|
|
|
class DoodleloopApp {
|
|
constructor() {
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.animationCanvas = null;
|
|
this.animationCtx = null;
|
|
this.originalImage = null;
|
|
this.motionStyle = 'ink-ripple';
|
|
this.intensity = 50;
|
|
this.speed = 50;
|
|
this.isAnimating = false;
|
|
this.animationFrameId = null;
|
|
this.sampleImageData = 'data:image/svg+xml;base64,' + btoa(`
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
|
|
<rect width="400" height="400" fill="#f0f8ff"/>
|
|
<circle cx="100" cy="100" r="40" fill="#FFD700"/>
|
|
<circle cx="90" cy="90" r="5" fill="#000"/>
|
|
<circle cx="110" cy="90" r="5" fill="#000"/>
|
|
<path d="M 85 120 Q 100 140 115 120" stroke="#000" fill="none" stroke-width="3"/>
|
|
<rect x="200" y="150" width="120" height="80" rx="10" fill="#8B4513"/>
|
|
<rect x="210" y="100" width="100" height="50" rx="5" fill="#ADD8E6"/>
|
|
<rect x="240" y="80" width="40" height="30" fill="#FF6347"/>
|
|
<polygon points="260,60 250,80 270,80" fill="#006400"/>
|
|
<text x="50" y="300" font-family="Arial" font-size="20" fill="#333">Draw something!</text>
|
|
<text x="50" y="330" font-family="Arial" font-size="16" fill="#666">Drop image or use sample</text>
|
|
</svg>
|
|
`);
|
|
|
|
this.motionStyles = {
|
|
'ink-ripple': { name: 'Ink Ripple', effect: this.inkRippleEffect.bind(this) },
|
|
'van-gogh-swirl': { name: 'Van Gogh Swirl', effect: this.vanGoghSwirlEffect.bind(this) },
|
|
'watercolor-bleed': { name: 'Watercolor Bleed', effect: this.watercolorBleedEffect.bind(this) },
|
|
'paper-cut-bob': { name: 'Paper-Cut Bob', effect: this.paperCutBobEffect.bind(this) },
|
|
'neon-pulse': { name: 'Neon Pulse', effect: this.neonPulseEffect.bind(this) },
|
|
'falling-snow': { name: 'Falling Snow', effect: this.fallingSnowEffect.bind(this) },
|
|
'fire-flicker': { name: 'Fire Flicker', effect: this.fireFlickerEffect.bind(this) },
|
|
'bubble-pop': { name: 'Bubble Pop', effect: this.bubblePopEffect.bind(this) },
|
|
'lightning-flash': { name: 'Lightning Flash', effect: this.lightningFlashEffect.bind(this) },
|
|
'earthquake-shake': { name: 'Earthquake Shake', effect: this.earthquakeShakeEffect.bind(this) },
|
|
'ocean-wave': { name: 'Ocean Wave', effect: this.oceanWaveEffect.bind(this) },
|
|
'magic-sparkle': { name: 'Magic Sparkle', effect: this.magicSparkleEffect.bind(this) }
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupElements();
|
|
this.setupEventListeners();
|
|
this.loadSampleImage();
|
|
}
|
|
|
|
setupElements() {
|
|
// Main canvas for original image
|
|
this.canvas = document.getElementById('original-canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
// Animation canvas for effects
|
|
this.animationCanvas = document.getElementById('animation-canvas');
|
|
this.animationCtx = this.animationCanvas.getContext('2d');
|
|
|
|
// Set canvas sizes
|
|
this.canvas.width = 400;
|
|
this.canvas.height = 400;
|
|
this.animationCanvas.width = 400;
|
|
this.animationCanvas.height = 400;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const dropZone = document.getElementById('drop-zone');
|
|
const fileInput = document.getElementById('file-input');
|
|
const styleSelect = document.getElementById('motion-style');
|
|
const intensitySlider = document.getElementById('intensity-slider');
|
|
const speedSlider = document.getElementById('speed-slider');
|
|
const exportBtn = document.getElementById('export-btn');
|
|
const sampleBtn = document.getElementById('sample-btn');
|
|
|
|
// Drag and drop events
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
|
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
this.handleFile(e.dataTransfer.files[0]);
|
|
}
|
|
});
|
|
|
|
// Click to upload
|
|
dropZone.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
this.handleFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
// Motion style selection
|
|
styleSelect.addEventListener('change', (e) => {
|
|
this.motionStyle = e.target.value;
|
|
if (this.originalImage) {
|
|
this.startAnimation();
|
|
}
|
|
});
|
|
|
|
// Slider events
|
|
intensitySlider.addEventListener('input', (e) => {
|
|
this.intensity = parseInt(e.target.value);
|
|
if (this.originalImage) {
|
|
this.startAnimation();
|
|
}
|
|
});
|
|
|
|
speedSlider.addEventListener('input', (e) => {
|
|
this.speed = parseInt(e.target.value);
|
|
if (this.originalImage) {
|
|
this.startAnimation();
|
|
}
|
|
});
|
|
|
|
// Sample button
|
|
sampleBtn.addEventListener('click', () => {
|
|
this.loadSampleImage();
|
|
});
|
|
|
|
// Export button
|
|
exportBtn.addEventListener('click', () => {
|
|
this.exportGIF();
|
|
});
|
|
}
|
|
|
|
handleFile(file) {
|
|
if (!file.type.match('image.*')) {
|
|
alert('Please select an image file');
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
this.loadImage(e.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
loadImage(src) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.originalImage = img;
|
|
this.drawOriginalImage();
|
|
this.startAnimation();
|
|
};
|
|
img.src = src;
|
|
}
|
|
|
|
loadSampleImage() {
|
|
this.loadImage(this.sampleImageData);
|
|
}
|
|
|
|
drawOriginalImage() {
|
|
if (!this.originalImage) return;
|
|
|
|
// Clear canvas
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Draw image centered and scaled to fit
|
|
const imgRatio = this.originalImage.width / this.originalImage.height;
|
|
const canvasRatio = this.canvas.width / this.canvas.height;
|
|
let drawWidth, drawHeight, offsetX, offsetY;
|
|
|
|
if (imgRatio > canvasRatio) {
|
|
// Image is wider relative to height
|
|
drawWidth = this.canvas.width;
|
|
drawHeight = this.canvas.width / imgRatio;
|
|
offsetX = 0;
|
|
offsetY = (this.canvas.height - drawHeight) / 2;
|
|
} else {
|
|
// Image is taller relative to width
|
|
drawHeight = this.canvas.height;
|
|
drawWidth = this.canvas.height * imgRatio;
|
|
offsetX = (this.canvas.width - drawWidth) / 2;
|
|
offsetY = 0;
|
|
}
|
|
|
|
this.ctx.drawImage(this.originalImage, offsetX, offsetY, drawWidth, drawHeight);
|
|
}
|
|
|
|
startAnimation() {
|
|
if (!this.originalImage) return;
|
|
|
|
// Cancel any ongoing animation
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
}
|
|
|
|
// Clear animation canvas
|
|
this.animationCtx.clearRect(0, 0, this.animationCanvas.width, this.animationCanvas.height);
|
|
|
|
// Start new animation
|
|
this.isAnimating = true;
|
|
this.animate();
|
|
}
|
|
|
|
animate() {
|
|
if (!this.isAnimating) return;
|
|
|
|
// Clear previous frame
|
|
this.animationCtx.clearRect(0, 0, this.animationCanvas.width, this.animationCanvas.height);
|
|
|
|
// Apply selected effect
|
|
const effect = this.motionStyles[this.motionStyle].effect;
|
|
effect();
|
|
|
|
// Continue animation loop
|
|
this.animationFrameId = requestAnimationFrame(() => this.animate());
|
|
}
|
|
|
|
// Motion Effects
|
|
|
|
inkRippleEffect() {
|
|
const time = Date.now() * (this.speed / 1000);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create ripple pattern
|
|
for (let i = 0; i < 5; i++) {
|
|
const radius = (time % 1000) / 10 + i * 30;
|
|
const alpha = Math.max(0, 1 - (radius / 100));
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(width/2, height/2, radius, 0, Math.PI * 2);
|
|
ctx.strokeStyle = `rgba(30, 144, 255, ${alpha})`;
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
vanGoghSwirlEffect() {
|
|
const time = Date.now() * (this.speed / 500);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
ctx.save();
|
|
ctx.translate(width/2, height/2);
|
|
ctx.rotate(Math.sin(time/100) * 0.2);
|
|
|
|
// Create swirling pattern
|
|
for (let i = 0; i < 20; i++) {
|
|
const angle = (i / 20) * Math.PI * 4 + time/100;
|
|
const radius = 20 + i * 5;
|
|
const x = Math.cos(angle) * radius;
|
|
const y = Math.sin(angle) * radius;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
|
ctx.fillStyle = `hsl(${angle * 20}, 80%, 60%)`;
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
watercolorBleedEffect() {
|
|
const time = Date.now() * (this.speed / 1000);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create soft bleeding colors
|
|
ctx.globalAlpha = 0.1;
|
|
for (let i = 0; i < 10; i++) {
|
|
const x = (Math.sin(time/200 + i) * width/3) + width/2;
|
|
const y = (Math.cos(time/150 + i) * height/3) + height/2;
|
|
const radius = 30 + Math.sin(time/100 + i) * 20;
|
|
|
|
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
gradient.addColorStop(0, `hsla(${(time/10 + i*30) % 360}, 80%, 60%, 0.8)`);
|
|
gradient.addColorStop(1, `hsla(${(time/10 + i*30) % 360}, 80%, 60%, 0)`);
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
paperCutBobEffect() {
|
|
const time = Date.now() * (this.speed / 500);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create bobbing paper cut effect
|
|
ctx.save();
|
|
ctx.translate(0, Math.sin(time/50) * 5);
|
|
|
|
// Draw some geometric shapes that bob
|
|
ctx.fillStyle = 'rgba(255, 99, 71, 0.7)';
|
|
ctx.fillRect(width/4, height/3, 50, 50);
|
|
|
|
ctx.fillStyle = 'rgba(60, 179, 113, 0.7)';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width/2, height/2);
|
|
ctx.lineTo(width/2 + 40, height/2 + 40);
|
|
ctx.lineTo(width/2 - 40, height/2 + 40);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
neonPulseEffect() {
|
|
const time = Date.now() * (this.speed / 200);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create pulsing neon glow
|
|
const pulse = Math.abs(Math.sin(time/100)) * 0.8 + 0.2;
|
|
|
|
ctx.shadowBlur = 20 * pulse;
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.strokeStyle = `rgba(0, 255, 255, ${pulse})`;
|
|
ctx.lineWidth = 3;
|
|
|
|
ctx.beginPath();
|
|
ctx.rect(50, 50, width-100, height-100);
|
|
ctx.stroke();
|
|
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
fallingSnowEffect() {
|
|
const time = Date.now() * (this.speed / 300);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create falling snow
|
|
for (let i = 0; i < 50; i++) {
|
|
const x = (Math.sin(time/50 + i) * width/2) + width/2;
|
|
const y = (time/5 + i * 20) % (height + 50);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'white';
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
fireFlickerEffect() {
|
|
const time = Date.now() * (this.speed / 100);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create flickering fire effect at bottom
|
|
ctx.globalCompositeOperation = 'screen';
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
const x = (i / 30) * width;
|
|
const flicker = Math.abs(Math.sin(time/100 + i));
|
|
const heightFactor = 30 + flicker * 40;
|
|
|
|
const gradient = ctx.createLinearGradient(x, height, x, height - heightFactor);
|
|
gradient.addColorStop(0, 'rgba(255, 100, 0, 0.8)');
|
|
gradient.addColorStop(0.5, 'rgba(255, 200, 0, 0.6)');
|
|
gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(x - 5, height - heightFactor, 10, heightFactor);
|
|
}
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
bubblePopEffect() {
|
|
const time = Date.now() * (this.speed / 200);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create popping bubbles
|
|
for (let i = 0; i < 20; i++) {
|
|
const phase = (time/100 + i) % 10;
|
|
const size = Math.min(phase * 5, 25);
|
|
const x = (i * 37) % width;
|
|
const y = height - ((time/20 + i * 10) % (height + 50));
|
|
|
|
if (phase < 5) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
ctx.strokeStyle = `rgba(173, 216, 230, ${1 - phase/5})`;
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
// Add shine
|
|
ctx.beginPath();
|
|
ctx.arc(x - size/3, y - size/3, size/4, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
lightningFlashEffect() {
|
|
const time = Date.now() * (this.speed / 100);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Random lightning flashes
|
|
if (Math.random() < 0.02) {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Lightning bolt
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width/2, 0);
|
|
ctx.lineTo(width/2 - 20, height/3);
|
|
ctx.lineTo(width/2 + 30, height/2);
|
|
ctx.lineTo(width/2 - 10, height*2/3);
|
|
ctx.lineTo(width/2 + 20, height);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
earthquakeShakeEffect() {
|
|
const time = Date.now() * (this.speed / 50);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Screen shake effect
|
|
const shakeX = Math.sin(time/10) * 3;
|
|
const shakeY = Math.cos(time/15) * 3;
|
|
|
|
ctx.save();
|
|
ctx.translate(shakeX, shakeY);
|
|
|
|
// Cracks appearing
|
|
ctx.strokeStyle = 'rgba(100, 100, 100, 0.7)';
|
|
ctx.lineWidth = 2;
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const offset = i * 50;
|
|
ctx.beginPath();
|
|
ctx.moveTo(offset, height/2 + Math.sin(time/20 + i) * 10);
|
|
ctx.lineTo(offset + 30, height/2 + Math.sin(time/20 + i + 1) * 15);
|
|
ctx.lineTo(offset + 60, height/2 + Math.sin(time/20 + i + 2) * 5);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
oceanWaveEffect() {
|
|
const time = Date.now() * (this.speed / 200);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Wave motion
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height - 50);
|
|
|
|
for (let x = 0; x <= width; x += 10) {
|
|
const y = height - 50 + Math.sin(x/30 + time/50) * 20;
|
|
ctx.lineTo(x, y);
|
|
}
|
|
|
|
ctx.lineTo(width, height);
|
|
ctx.lineTo(0, height);
|
|
ctx.closePath();
|
|
|
|
const gradient = ctx.createLinearGradient(0, height - 70, 0, height);
|
|
gradient.addColorStop(0, 'rgba(30, 144, 255, 0.6)');
|
|
gradient.addColorStop(1, 'rgba(0, 100, 200, 0.3)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fill();
|
|
}
|
|
|
|
magicSparkleEffect() {
|
|
const time = Date.now() * (this.speed / 100);
|
|
const ctx = this.animationCtx;
|
|
const width = this.animationCanvas.width;
|
|
const height = this.animationCanvas.height;
|
|
|
|
// Create magical sparkles
|
|
for (let i = 0; i < 30; i++) {
|
|
const x = (i * 47) % width;
|
|
const y = (i * 31) % height;
|
|
const sparkle = Math.abs(Math.sin(time/100 + i));
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, sparkle * 3, 0, Math.PI * 2);
|
|
ctx.fillStyle = `hsla(${(time/10 + i*12) % 360}, 100%, 70%, ${sparkle})`;
|
|
ctx.fill();
|
|
|
|
// Star shape
|
|
ctx.beginPath();
|
|
for (let j = 0; j < 5; j++) {
|
|
const angle = (j * 2 * Math.PI / 5) - Math.PI/2;
|
|
const outerRadius = sparkle * 5;
|
|
const innerRadius = sparkle * 2;
|
|
|
|
const outerX = x + Math.cos(angle) * outerRadius;
|
|
const outerY = y + Math.sin(angle) * outerRadius;
|
|
const innerAngle = angle + Math.PI / 5;
|
|
const innerX = x + Math.cos(innerAngle) * innerRadius;
|
|
const innerY = y + Math.sin(innerAngle) * innerRadius;
|
|
|
|
if (j === 0) {
|
|
ctx.moveTo(outerX, outerY);
|
|
} else {
|
|
ctx.lineTo(innerX, innerY);
|
|
}
|
|
ctx.lineTo(outerX, outerY);
|
|
}
|
|
ctx.closePath();
|
|
ctx.fillStyle = `rgba(255, 215, 0, ${sparkle * 0.7})`;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
exportGIF() {
|
|
alert('GIF export would be implemented here. In a full implementation, this would generate an animated GIF using the canvas frames.');
|
|
// In a real implementation, we would use a library like gif.js to encode the animation
|
|
}
|
|
}
|
|
|
|
// Initialize the app when the page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.doodleloopApp = new DoodleloopApp();
|
|
}); |