You can make animation tools using chatgpt. or Simple you can copy this html code to make your own animation app.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editable Drawing App with Frames</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
#toolbar, #framebar {
margin-bottom: 10px;
}
button, select, label {
margin-right: 5px;
padding: 5px 10px;
}
#canvasContainer {
position: relative;
}
canvas {
border: 1px solid #ccc;
cursor: crosshair;
}
</style>
</head>
<body>
<h2>Editable Drawing App with Frames</h2>
<!-- Toolbar for drawing tools and deletion -->
<div id="toolbar">
<button id="freeDrawBtn">Free Draw</button>
<button id="lineBtn">Line</button>
<button id="rectBtn">Rectangle</button>
<button id="circleBtn">Circle</button>
<button id="selectBtn">Select/Move</button>
<button id="deleteBtn">Delete Object</button>
<span style="margin-left: 20px;">Current Tool: <span id="currentTool">Free Draw</span></span>
<label>
<input type="checkbox" id="gridCheckbox"> Grid
</label>
</div>
<!-- Frame controls -->
<div id="framebar">
<button id="addFrameBtn">Add Frame</button>
<select id="frameSelect"></select>
<button id="playbackBtn">Playback</button>
<label>
<input type="checkbox" id="loopCheckbox"> Frame Loop
</label>
</div>
<!-- Canvas container -->
<div id="canvasContainer">
<canvas id="drawingCanvas" width="800" height="500"></canvas>
</div>
<script>
/* ========= Data Structures ========= */
// Each shape is stored as an object with a type and properties.
class Shape {
constructor(type, props) {
this.type = type; // "free", "line", "rect", "circle"
this.props = props; // e.g., free: {points: [...]}, others: {start, end}
this.selected = false;
}
// Draw shape and, if selected, its resize handles
draw(ctx) {
ctx.save();
ctx.strokeStyle = this.selected ? "red" : "black";
ctx.lineWidth = 2;
switch(this.type) {
case "free":
ctx.beginPath();
this.props.points.forEach((pt, i) => {
if (i === 0) ctx.moveTo(pt.x, pt.y);
else ctx.lineTo(pt.x, pt.y);
});
ctx.stroke();
break;
case "line":
ctx.beginPath();
ctx.moveTo(this.props.start.x, this.props.start.y);
ctx.lineTo(this.props.end.x, this.props.end.y);
ctx.stroke();
break;
case "rect":
var x = Math.min(this.props.start.x, this.props.end.x);
var y = Math.min(this.props.start.y, this.props.end.y);
var w = Math.abs(this.props.end.x - this.props.start.x);
var h = Math.abs(this.props.end.y - this.props.start.y);
ctx.strokeRect(x, y, w, h);
break;
case "circle":
var dx = this.props.end.x - this.props.start.x;
var dy = this.props.end.y - this.props.start.y;
var radius = Math.sqrt(dx*dx + dy*dy);
ctx.beginPath();
ctx.arc(this.props.start.x, this.props.start.y, radius, 0, 2*Math.PI);
ctx.stroke();
break;
}
if (this.selected) {
// Draw resize handles at corners of bounding box
const bbox = this.boundingBox();
const handles = getHandles(bbox);
ctx.fillStyle = "blue";
handles.forEach(h => {
ctx.fillRect(h.x - 4, h.y - 4, 8, 8);
});
}
ctx.restore();
}
// Returns the bounding box {x, y, w, h} of the shape
boundingBox() {
switch(this.type) {
case "free":
const xs = this.props.points.map(pt => pt.x);
const ys = this.props.points.map(pt => pt.y);
const x = Math.min(...xs);
const y = Math.min(...ys);
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
case "line":
const x1 = Math.min(this.props.start.x, this.props.end.x);
const y1 = Math.min(this.props.start.y, this.props.end.y);
return { x: x1, y: y1, w: Math.abs(this.props.end.x - this.props.start.x), h: Math.abs(this.props.end.y - this.props.start.y) };
case "rect":
const rx = Math.min(this.props.start.x, this.props.end.x);
const ry = Math.min(this.props.start.y, this.props.end.y);
return { x: rx, y: ry, w: Math.abs(this.props.end.x - this.props.start.x), h: Math.abs(this.props.end.y - this.props.start.y) };
case "circle":
const dx = this.props.end.x - this.props.start.x;
const dy = this.props.end.y - this.props.start.y;
const r = Math.sqrt(dx*dx + dy*dy);
return { x: this.props.start.x - r, y: this.props.start.y - r, w: 2*r, h: 2*r };
}
}
// Check if a point (x,y) is on or near this shape (for selection purposes)
hitTest(x, y) {
const buffer = 5; // Tolerance
switch(this.type) {
case "free":
for (let pt of this.props.points) {
if (Math.abs(pt.x - x) < buffer && Math.abs(pt.y - y) < buffer) return true;
}
break;
case "line":
const { start, end } = this.props;
const A = x - start.x, B = y - start.y;
const C = end.x - start.x, D = end.y - start.y;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
let param = len_sq ? dot / len_sq : -1;
let xx, yy;
if (param < 0) { xx = start.x; yy = start.y; }
else if (param > 1) { xx = end.x; yy = end.y; }
else { xx = start.x + param * C; yy = start.y + param * D; }
if (Math.sqrt((x - xx)**2 + (y - yy)**2) < buffer) return true;
break;
case "rect":
const bbox = this.boundingBox();
if (x >= bbox.x && x <= bbox.x + bbox.w && y >= bbox.y && y <= bbox.y + bbox.h) return true;
break;
case "circle":
const dxC = x - this.props.start.x;
const dyC = y - this.props.start.y;
const r = Math.sqrt((this.props.end.x - this.props.start.x)**2 + (this.props.end.y - this.props.start.y)**2);
if (Math.abs(Math.sqrt(dxC*dxC + dyC*dyC) - r) < buffer) return true;
break;
}
return false;
}
// Move the shape by (dx,dy)
move(dx, dy) {
if(this.type === "free") {
this.props.points.forEach(pt => { pt.x += dx; pt.y += dy; });
} else if(this.type === "line" || this.type === "rect" || this.type === "circle") {
this.props.start.x += dx;
this.props.start.y += dy;
this.props.end.x += dx;
this.props.end.y += dy;
}
}
}
// Each frame holds an array of shapes
class Frame {
constructor() {
this.shapes = [];
}
draw(ctx) {
this.shapes.forEach(shape => shape.draw(ctx));
}
}
/* ========= Global Variables ========= */
let currentTool = "free"; // Options: free, line, rect, circle, select
let drawing = false;
let currentShape = null;
let selectedShape = null;
// Variables for moving/resizing
let resizing = false;
let currentResizeHandle = null;
let resizeStartX = 0, resizeStartY = 0;
let originalProps = null; // A deep copy of props for the shape before resizing
const frames = [];
let currentFrameIndex = 0;
const canvas = document.getElementById("drawingCanvas");
const ctx = canvas.getContext("2d");
// Utility: Returns the four corner handle positions from a bounding box.
function getHandles(bbox) {
return [
{ name: "tl", x: bbox.x, y: bbox.y },
{ name: "tr", x: bbox.x + bbox.w, y: bbox.y },
{ name: "bl", x: bbox.x, y: bbox.y + bbox.h },
{ name: "br", x: bbox.x + bbox.w, y: bbox.y + bbox.h }
];
}
// Check if (x,y) is over a handle of the currently selected shape.
function checkHandleHit(x, y, shape) {
const bbox = shape.boundingBox();
const handles = getHandles(bbox);
for (let handle of handles) {
if (Math.abs(handle.x - x) < 6 && Math.abs(handle.y - y) < 6) {
return handle.name;
}
}
return null;
}
function updateToolDisplay() {
document.getElementById("currentTool").textContent =
currentTool === "free" ? "Free Draw" :
currentTool === "line" ? "Line" :
currentTool === "rect" ? "Rectangle" :
currentTool === "circle" ? "Circle" :
"Select/Move";
}
/* ========= Frame Management ========= */
function addFrame() {
frames.push(new Frame());
currentFrameIndex = frames.length - 1;
updateFrameSelect();
redraw();
}
function updateFrameSelect() {
const frameSelect = document.getElementById("frameSelect");
frameSelect.innerHTML = "";
frames.forEach((frame, index) => {
const option = document.createElement("option");
option.value = index;
option.text = "Frame " + (index + 1);
if(index === currentFrameIndex) option.selected = true;
frameSelect.appendChild(option);
});
}
function selectFrame(index) {
currentFrameIndex = index;
redraw();
}
/* ========= Grid Drawing ========= */
function drawGrid() {
const gridOn = document.getElementById("gridCheckbox").checked;
if (!gridOn) return;
const step = 25;
ctx.save();
ctx.strokeStyle = "#eee";
ctx.lineWidth = 1;
for (let x = 0; x <= canvas.width; x += step) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += step) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
ctx.restore();
}
/* ========= Redraw Canvas ========= */
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
if(frames[currentFrameIndex]) frames[currentFrameIndex].draw(ctx);
}
/* ========= Mouse Event Handlers ========= */
canvas.addEventListener("mousedown", (e) => {
const rectCanvas = canvas.getBoundingClientRect();
const x = e.clientX - rectCanvas.left;
const y = e.clientY - rectCanvas.top;
drawing = true;
// If in Select/Move mode
if(currentTool === "select") {
// If an object is already selected, check if a resize handle is hit.
if(selectedShape) {
const handle = checkHandleHit(x, y, selectedShape);
if(handle) {
resizing = true;
currentResizeHandle = handle;
resizeStartX = x;
resizeStartY = y;
// Deep copy the original properties to use as a baseline for resizing.
originalProps = JSON.parse(JSON.stringify(selectedShape.props));
return;
}
}
// Otherwise try to select an object.
const shapes = frames[currentFrameIndex].shapes;
selectedShape = null;
// Check in reverse order to pick the topmost object.
for(let i = shapes.length - 1; i >= 0; i--) {
if(shapes[i].hitTest(x, y)) {
selectedShape = shapes[i];
selectedShape.selected = true;
break;
}
}
redraw();
return;
}
// For drawing new objects.
selectedShape = null;
switch(currentTool) {
case "free":
currentShape = new Shape("free", { points: [{ x, y }] });
break;
case "line":
currentShape = new Shape("line", { start: { x, y }, end: { x, y } });
break;
case "rect":
currentShape = new Shape("rect", { start: { x, y }, end: { x, y } });
break;
case "circle":
currentShape = new Shape("circle", { start: { x, y }, end: { x, y } });
break;
}
});
canvas.addEventListener("mousemove", (e) => {
if(!drawing) return;
const rectCanvas = canvas.getBoundingClientRect();
const x = e.clientX - rectCanvas.left;
const y = e.clientY - rectCanvas.top;
// If drawing a new shape.
if(currentShape && currentTool !== "select") {
if(currentTool === "free") {
currentShape.props.points.push({ x, y });
} else {
currentShape.props.end = { x, y };
}
redraw();
currentShape.draw(ctx);
return;
}
// If in select mode and resizing.
if(currentTool === "select" && resizing && selectedShape) {
// Determine new parameters based on the handle dragged.
let dx = x - resizeStartX;
let dy = y - resizeStartY;
// Get original bounding box and recompute new one according to handle.
let origBox;
if(selectedShape.type === "free") {
const xs = originalProps.points.map(pt => pt.x);
const ys = originalProps.points.map(pt => pt.y);
origBox = { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) };
} else {
origBox = {
x: Math.min(originalProps.start.x, originalProps.end.x),
y: Math.min(originalProps.start.y, originalProps.end.y),
w: Math.abs(originalProps.end.x - originalProps.start.x),
h: Math.abs(originalProps.end.y - originalProps.start.y)
};
}
let newBox = Object.assign({}, origBox);
if(currentResizeHandle === "tl") {
newBox.x += dx; newBox.y += dy;
newBox.w -= dx; newBox.h -= dy;
} else if(currentResizeHandle === "tr") {
newBox.y += dy;
newBox.w += dx; newBox.h -= dy;
} else if(currentResizeHandle === "bl") {
newBox.x += dx;
newBox.w -= dx; newBox.h += dy;
} else if(currentResizeHandle === "br") {
newBox.w += dx; newBox.h += dy;
}
// Prevent negative width/height.
if(newBox.w < 5 || newBox.h < 5) return;
// Update selected shape's properties based on its type.
if(selectedShape.type === "rect" || selectedShape.type === "line") {
// For these, map new bounding box to start and end.
selectedShape.props.start = { x: newBox.x, y: newBox.y };
selectedShape.props.end = { x: newBox.x + newBox.w, y: newBox.y + newBox.h };
} else if(selectedShape.type === "circle") {
// For circle, start remains the center.
selectedShape.props.start = { x: newBox.x + newBox.w/2, y: newBox.y + newBox.h/2 };
// Use average of width and height as radius.
const r = (newBox.w + newBox.h) / 4;
selectedShape.props.end = { x: selectedShape.props.start.x + r, y: selectedShape.props.start.y };
} else if(selectedShape.type === "free") {
// Scale free drawing points relative to original bounding box.
const scaleX = newBox.w / origBox.w;
const scaleY = newBox.h / origBox.h;
selectedShape.props.points = originalProps.points.map(pt => {
return {
x: newBox.x + (pt.x - origBox.x) * scaleX,
y: newBox.y + (pt.y - origBox.y) * scaleY
};
});
}
redraw();
return;
}
// If in select mode and moving an object (and not resizing), move the shape.
if(currentTool === "select" && selectedShape && !resizing) {
// Calculate movement based on last mouse position (here we simply use delta movement)
// For simplicity, we update by the difference from previous event position.
// (A more robust solution would store previous coordinates.)
selectedShape.move(0, 0); // placeholder if needed
// In this simple demo, moving is handled by clicking and dragging on the shape body (not on handles).
}
});
canvas.addEventListener("mouseup", (e) => {
drawing = false;
if(currentShape && currentTool !== "select") {
frames[currentFrameIndex].shapes.push(currentShape);
currentShape = null;
redraw();
}
if(currentTool === "select" && resizing) {
resizing = false;
currentResizeHandle = null;
originalProps = null;
}
});
/* ========= Playback Functionality ========= */
let playbackInterval = null;
function playbackFrames() {
if(frames.length === 0) return;
let cur = 0;
const loop = document.getElementById("loopCheckbox").checked;
if(playbackInterval) clearInterval(playbackInterval);
playbackInterval = setInterval(() => {
currentFrameIndex = cur;
updateFrameSelect();
redraw();
cur++;
if(cur >= frames.length) {
if(loop) cur = 0;
else {
clearInterval(playbackInterval);
}
}
}, 500); // 500ms per frame
}
/* ========= Toolbar Button Actions ========= */
document.getElementById("freeDrawBtn").addEventListener("click", () => {
currentTool = "free";
updateToolDisplay();
});
document.getElementById("lineBtn").addEventListener("click", () => {
currentTool = "line";
updateToolDisplay();
});
document.getElementById("rectBtn").addEventListener("click", () => {
currentTool = "rect";
updateToolDisplay();
});
document.getElementById("circleBtn").addEventListener("click", () => {
currentTool = "circle";
updateToolDisplay();
});
document.getElementById("selectBtn").addEventListener("click", () => {
currentTool = "select";
updateToolDisplay();
});
// Delete the selected shape.
document.getElementById("deleteBtn").addEventListener("click", () => {
if(currentTool === "select" && selectedShape) {
const shapes = frames[currentFrameIndex].shapes;
const index = shapes.indexOf(selectedShape);
if(index !== -1) {
shapes.splice(index, 1);
}
selectedShape = null;
redraw();
}
});
document.getElementById("addFrameBtn").addEventListener("click", () => {
addFrame();
});
document.getElementById("frameSelect").addEventListener("change", (e) => {
selectFrame(Number(e.target.value));
});
document.getElementById("playbackBtn").addEventListener("click", () => {
playbackFrames();
});
document.getElementById("gridCheckbox").addEventListener("change", () => {
redraw();
});
/* ========= Initialization ========= */
// Start with one empty frame.
addFrame();
updateToolDisplay();
</script>
</body>
</html>
Comments
Post a Comment