import { Gesture } from "@/gestures" import { Editor, TLDrawShape, TLShape, VecLike, createShapeId } from "tldraw" const getShapesUnderGesture = (editor: Editor, gesture: TLDrawShape) => { const bounds = editor.getShapePageBounds(gesture.id) return editor.getShapesAtPoint(bounds?.center!, { margin: (bounds?.width! + bounds?.height!) / 4, }).filter((shape) => shape.id !== gesture.id) } /** Returns shapes arranged in a circle around the given origin */ const circleDistribution = (editor: Editor, shapes: TLShape[], origin: VecLike, radius: number): TLShape[] => { const angleStep = (2 * Math.PI) / shapes.length; return shapes.map((shape, index) => { const { w, h } = editor.getShapeGeometry(shape.id).bounds const angle = index * angleStep; const pointOnCircle = { x: origin.x + radius * Math.cos(angle), y: origin.y + radius * Math.sin(angle), }; const shapeAngle = angle + Math.PI / 2; const pos = posFromRotatedCenter(pointOnCircle, w, h, shapeAngle); return { ...shape, x: pos.x, y: pos.y, rotation: shapeAngle, }; }); } /** Returns shapes arranged in a triangle around the given origin */ const triangleDistribution = (editor: Editor, shapes: TLShape[], origin: VecLike, radius: number): TLShape[] => { const vertices = [ { x: origin.x - radius, y: origin.y + radius }, // Bottom left { x: origin.x + radius, y: origin.y + radius }, // Bottom right { x: origin.x, y: origin.y - radius }, // Top middle ]; const totalShapes = shapes.length; const shapesPerEdge = Math.ceil(totalShapes / 3); return shapes.map((shape, index) => { const edgeIndex = Math.floor(index / shapesPerEdge); const edgeStart = vertices[edgeIndex]; const edgeEnd = vertices[(edgeIndex + 1) % 3]; const t = (index % shapesPerEdge) / shapesPerEdge; const pointOnEdge = { x: edgeStart.x + t * (edgeEnd.x - edgeStart.x), y: edgeStart.y + t * (edgeEnd.y - edgeStart.y), }; let shapeAngle; if (index % shapesPerEdge === 0) { // Shape is at a vertex, adjust angle to face away from the triangle const vertex = vertices[edgeIndex]; shapeAngle = Math.atan2(vertex.y - origin.y, vertex.x - origin.x); } else { // Shape is on an edge shapeAngle = Math.atan2(edgeEnd.y - edgeStart.y, edgeEnd.x - edgeStart.x); } const { w, h } = editor.getShapeGeometry(shape.id).bounds; const pos = posFromRotatedCenter(pointOnEdge, w, h, shapeAngle); return { ...shape, x: pos.x, y: pos.y, rotation: shapeAngle, }; }); } /** Calculates the top-left position of a shape given a center point, width, height, and rotation (radians) */ /** its origin x/y and rotation are around the top-left corner of the shape */ const posFromRotatedCenter = (center: VecLike, w: number, h: number, rotation: number): VecLike => { const halfWidth = w / 2; const halfHeight = h / 2; const cosTheta = Math.cos(rotation); const sinTheta = Math.sin(rotation); const topLeftX = center.x - (halfWidth * cosTheta - halfHeight * sinTheta); const topLeftY = center.y - (halfWidth * sinTheta + halfHeight * cosTheta); return { x: topLeftX, y: topLeftY }; } export const DEFAULT_GESTURES: Gesture[] = [ { name: "x", onComplete(editor) { editor.deleteShapes(editor.getSelectedShapes()) }, points: [ { x: 87, y: 142 }, { x: 89, y: 145 }, { x: 91, y: 148 }, { x: 93, y: 151 }, { x: 96, y: 155 }, { x: 98, y: 157 }, { x: 100, y: 160 }, { x: 102, y: 162 }, { x: 106, y: 167 }, { x: 108, y: 169 }, { x: 110, y: 171 }, { x: 115, y: 177 }, { x: 119, y: 183 }, { x: 123, y: 189 }, { x: 127, y: 193 }, { x: 129, y: 196 }, { x: 133, y: 200 }, { x: 137, y: 206 }, { x: 140, y: 209 }, { x: 143, y: 212 }, { x: 146, y: 215 }, { x: 151, y: 220 }, { x: 153, y: 222 }, { x: 155, y: 223 }, { x: 157, y: 225 }, { x: 158, y: 223 }, { x: 157, y: 218 }, { x: 155, y: 211 }, { x: 154, y: 208 }, { x: 152, y: 200 }, { x: 150, y: 189 }, { x: 148, y: 179 }, { x: 147, y: 170 }, { x: 147, y: 158 }, { x: 147, y: 148 }, { x: 147, y: 141 }, { x: 147, y: 136 }, { x: 144, y: 135 }, { x: 142, y: 137 }, { x: 140, y: 139 }, { x: 135, y: 145 }, { x: 131, y: 152 }, { x: 124, y: 163 }, { x: 116, y: 177 }, { x: 108, y: 191 }, { x: 100, y: 206 }, { x: 94, y: 217 }, { x: 91, y: 222 }, { x: 89, y: 225 }, { x: 87, y: 226 }, { x: 87, y: 224 }, ], }, { name: "rectangle", onComplete(editor, gesture?: TLDrawShape) { const bounds = editor.getShapePageBounds(gesture?.id!) const { w, h, center } = bounds! editor.createShape({ id: createShapeId(), type: "geo", x: center?.x! - w / 2, y: center?.y! - h / 2, isLocked: false, props: { fill: "solid", w: w, h: h, geo: "rectangle", dash: "draw", size: "m", font: "draw", align: "middle", verticalAlign: "middle", growY: 0, url: "", scale: 1, labelColor: "black", richText: [] as any }, }) }, points: [ { x: 78, y: 149 }, { x: 78, y: 153 }, { x: 78, y: 157 }, { x: 78, y: 160 }, { x: 79, y: 162 }, { x: 79, y: 164 }, { x: 79, y: 167 }, { x: 79, y: 169 }, { x: 79, y: 173 }, { x: 79, y: 178 }, { x: 79, y: 183 }, { x: 80, y: 189 }, { x: 80, y: 193 }, { x: 80, y: 198 }, { x: 80, y: 202 }, { x: 81, y: 208 }, { x: 81, y: 210 }, { x: 81, y: 216 }, { x: 82, y: 222 }, { x: 82, y: 224 }, { x: 82, y: 227 }, { x: 83, y: 229 }, { x: 83, y: 231 }, { x: 85, y: 230 }, { x: 88, y: 232 }, { x: 90, y: 233 }, { x: 92, y: 232 }, { x: 94, y: 233 }, { x: 99, y: 232 }, { x: 102, y: 233 }, { x: 106, y: 233 }, { x: 109, y: 234 }, { x: 117, y: 235 }, { x: 123, y: 236 }, { x: 126, y: 236 }, { x: 135, y: 237 }, { x: 142, y: 238 }, { x: 145, y: 238 }, { x: 152, y: 238 }, { x: 154, y: 239 }, { x: 165, y: 238 }, { x: 174, y: 237 }, { x: 179, y: 236 }, { x: 186, y: 235 }, { x: 191, y: 235 }, { x: 195, y: 233 }, { x: 197, y: 233 }, { x: 200, y: 233 }, { x: 201, y: 235 }, { x: 201, y: 233 }, { x: 199, y: 231 }, { x: 198, y: 226 }, { x: 198, y: 220 }, { x: 196, y: 207 }, { x: 195, y: 195 }, { x: 195, y: 181 }, { x: 195, y: 173 }, { x: 195, y: 163 }, { x: 194, y: 155 }, { x: 192, y: 145 }, { x: 192, y: 143 }, { x: 192, y: 138 }, { x: 191, y: 135 }, { x: 191, y: 133 }, { x: 191, y: 130 }, { x: 190, y: 128 }, { x: 188, y: 129 }, { x: 186, y: 129 }, { x: 181, y: 132 }, { x: 173, y: 131 }, { x: 162, y: 131 }, { x: 151, y: 132 }, { x: 149, y: 132 }, { x: 138, y: 132 }, { x: 136, y: 132 }, { x: 122, y: 131 }, { x: 120, y: 131 }, { x: 109, y: 130 }, { x: 107, y: 130 }, { x: 90, y: 132 }, { x: 81, y: 133 }, { x: 76, y: 133 }, ], }, { name: "circle", onComplete(editor, gesture?: TLDrawShape) { const selection = getShapesUnderGesture(editor, gesture!) editor.setSelectedShapes(selection) editor.setHintingShapes(selection) }, points: [ { x: 127, y: 141 }, { x: 124, y: 140 }, { x: 120, y: 139 }, { x: 118, y: 139 }, { x: 116, y: 139 }, { x: 111, y: 140 }, { x: 109, y: 141 }, { x: 104, y: 144 }, { x: 100, y: 147 }, { x: 96, y: 152 }, { x: 93, y: 157 }, { x: 90, y: 163 }, { x: 87, y: 169 }, { x: 85, y: 175 }, { x: 83, y: 181 }, { x: 82, y: 190 }, { x: 82, y: 195 }, { x: 83, y: 200 }, { x: 84, y: 205 }, { x: 88, y: 213 }, { x: 91, y: 216 }, { x: 96, y: 219 }, { x: 103, y: 222 }, { x: 108, y: 224 }, { x: 111, y: 224 }, { x: 120, y: 224 }, { x: 133, y: 223 }, { x: 142, y: 222 }, { x: 152, y: 218 }, { x: 160, y: 214 }, { x: 167, y: 210 }, { x: 173, y: 204 }, { x: 178, y: 198 }, { x: 179, y: 196 }, { x: 182, y: 188 }, { x: 182, y: 177 }, { x: 178, y: 167 }, { x: 170, y: 150 }, { x: 163, y: 138 }, { x: 152, y: 130 }, { x: 143, y: 129 }, { x: 140, y: 131 }, { x: 129, y: 136 }, { x: 126, y: 139 }, ], }, { name: "check", onComplete(editor, gesture?: TLDrawShape) { const originPoint = { x: gesture?.x!, y: gesture?.y! } const shapeAtOrigin = editor.getShapesAtPoint(originPoint, { hitInside: true, margin: 10, }) for (const shape of shapeAtOrigin) { if (shape.id === gesture?.id) continue editor.updateShape({ ...shape, props: { ...shape.props, color: "green", }, }) } }, points: [ { x: 91, y: 185 }, { x: 93, y: 185 }, { x: 95, y: 185 }, { x: 97, y: 185 }, { x: 100, y: 188 }, { x: 102, y: 189 }, { x: 104, y: 190 }, { x: 106, y: 193 }, { x: 108, y: 195 }, { x: 110, y: 198 }, { x: 112, y: 201 }, { x: 114, y: 204 }, { x: 115, y: 207 }, { x: 117, y: 210 }, { x: 118, y: 212 }, { x: 120, y: 214 }, { x: 121, y: 217 }, { x: 122, y: 219 }, { x: 123, y: 222 }, { x: 124, y: 224 }, { x: 126, y: 226 }, { x: 127, y: 229 }, { x: 129, y: 231 }, { x: 130, y: 233 }, { x: 129, y: 231 }, { x: 129, y: 228 }, { x: 129, y: 226 }, { x: 129, y: 224 }, { x: 129, y: 221 }, { x: 129, y: 218 }, { x: 129, y: 212 }, { x: 129, y: 208 }, { x: 130, y: 198 }, { x: 132, y: 189 }, { x: 134, y: 182 }, { x: 137, y: 173 }, { x: 143, y: 164 }, { x: 147, y: 157 }, { x: 151, y: 151 }, { x: 155, y: 144 }, { x: 161, y: 137 }, { x: 165, y: 131 }, { x: 171, y: 122 }, { x: 174, y: 118 }, { x: 176, y: 114 }, { x: 177, y: 112 }, { x: 177, y: 114 }, { x: 175, y: 116 }, { x: 173, y: 118 }, ], }, { name: "caret", onComplete(editor) { editor.alignShapes(editor.getSelectedShapes(), "top") }, points: [ { x: 79, y: 245 }, { x: 79, y: 242 }, { x: 79, y: 239 }, { x: 80, y: 237 }, { x: 80, y: 234 }, { x: 81, y: 232 }, { x: 82, y: 230 }, { x: 84, y: 224 }, { x: 86, y: 220 }, { x: 86, y: 218 }, { x: 87, y: 216 }, { x: 88, y: 213 }, { x: 90, y: 207 }, { x: 91, y: 202 }, { x: 92, y: 200 }, { x: 93, y: 194 }, { x: 94, y: 192 }, { x: 96, y: 189 }, { x: 97, y: 186 }, { x: 100, y: 179 }, { x: 102, y: 173 }, { x: 105, y: 165 }, { x: 107, y: 160 }, { x: 109, y: 158 }, { x: 112, y: 151 }, { x: 115, y: 144 }, { x: 117, y: 139 }, { x: 119, y: 136 }, { x: 119, y: 134 }, { x: 120, y: 132 }, { x: 121, y: 129 }, { x: 122, y: 127 }, { x: 124, y: 125 }, { x: 126, y: 124 }, { x: 129, y: 125 }, { x: 131, y: 127 }, { x: 132, y: 130 }, { x: 136, y: 139 }, { x: 141, y: 154 }, { x: 145, y: 166 }, { x: 151, y: 182 }, { x: 156, y: 193 }, { x: 157, y: 196 }, { x: 161, y: 209 }, { x: 162, y: 211 }, { x: 167, y: 223 }, { x: 169, y: 229 }, { x: 170, y: 231 }, { x: 173, y: 237 }, { x: 176, y: 242 }, { x: 177, y: 244 }, { x: 179, y: 250 }, { x: 181, y: 255 }, { x: 182, y: 257 }, ], }, // { // name: "zig-zag", // points: [ // { x: 307, y: 216 }, // { x: 333, y: 186 }, // { x: 356, y: 215 }, // { x: 375, y: 186 }, // { x: 399, y: 216 }, // { x: 418, y: 186 }, // ], // }, { name: "v", onComplete(editor) { editor.alignShapes(editor.getSelectedShapes(), "bottom") }, points: [ { x: 89, y: 164 }, { x: 90, y: 162 }, { x: 92, y: 162 }, { x: 94, y: 164 }, { x: 95, y: 166 }, { x: 96, y: 169 }, { x: 97, y: 171 }, { x: 99, y: 175 }, { x: 101, y: 178 }, { x: 103, y: 182 }, { x: 106, y: 189 }, { x: 108, y: 194 }, { x: 111, y: 199 }, { x: 114, y: 204 }, { x: 117, y: 209 }, { x: 119, y: 214 }, { x: 122, y: 218 }, { x: 124, y: 222 }, { x: 126, y: 225 }, { x: 128, y: 228 }, { x: 130, y: 229 }, { x: 133, y: 233 }, { x: 134, y: 236 }, { x: 136, y: 239 }, { x: 138, y: 240 }, { x: 139, y: 242 }, { x: 140, y: 244 }, { x: 142, y: 242 }, { x: 142, y: 240 }, { x: 142, y: 237 }, { x: 143, y: 235 }, { x: 143, y: 233 }, { x: 145, y: 229 }, { x: 146, y: 226 }, { x: 148, y: 217 }, { x: 149, y: 208 }, { x: 149, y: 205 }, { x: 151, y: 196 }, { x: 151, y: 193 }, { x: 153, y: 182 }, { x: 155, y: 172 }, { x: 157, y: 165 }, { x: 159, y: 160 }, { x: 162, y: 155 }, { x: 164, y: 150 }, { x: 165, y: 148 }, { x: 166, y: 146 }, ], }, { name: "delete", onComplete(editor) { editor.deleteShapes(editor.getSelectedShapes()) }, points: [ { x: 123, y: 129 }, { x: 123, y: 131 }, { x: 124, y: 133 }, { x: 125, y: 136 }, { x: 127, y: 140 }, { x: 129, y: 142 }, { x: 133, y: 148 }, { x: 137, y: 154 }, { x: 143, y: 158 }, { x: 145, y: 161 }, { x: 148, y: 164 }, { x: 153, y: 170 }, { x: 158, y: 176 }, { x: 160, y: 178 }, { x: 164, y: 183 }, { x: 168, y: 188 }, { x: 171, y: 191 }, { x: 175, y: 196 }, { x: 178, y: 200 }, { x: 180, y: 202 }, { x: 181, y: 205 }, { x: 184, y: 208 }, { x: 186, y: 210 }, { x: 187, y: 213 }, { x: 188, y: 215 }, { x: 186, y: 212 }, { x: 183, y: 211 }, { x: 177, y: 208 }, { x: 169, y: 206 }, { x: 162, y: 205 }, { x: 154, y: 207 }, { x: 145, y: 209 }, { x: 137, y: 210 }, { x: 129, y: 214 }, { x: 122, y: 217 }, { x: 118, y: 218 }, { x: 111, y: 221 }, { x: 109, y: 222 }, { x: 110, y: 219 }, { x: 112, y: 217 }, { x: 118, y: 209 }, { x: 120, y: 207 }, { x: 128, y: 196 }, { x: 135, y: 187 }, { x: 138, y: 183 }, { x: 148, y: 167 }, { x: 157, y: 153 }, { x: 163, y: 145 }, { x: 165, y: 142 }, { x: 172, y: 133 }, { x: 177, y: 127 }, { x: 179, y: 127 }, { x: 180, y: 125 }, ], }, { name: "pigtail", onComplete(editor, gesture?: TLDrawShape) { const shapes = getShapesUnderGesture(editor, gesture!) editor.setSelectedShapes(shapes) editor.setHintingShapes(shapes) editor.animateShapes(shapes.map((shape) => ({ ...shape, rotation: shape.rotation + (Math.PI / -2), })), { animation: { duration: 600, easing: (t) => t * t * (3 - 2 * t), }, }, ) }, points: [ { x: 81, y: 219 }, { x: 84, y: 218 }, { x: 86, y: 220 }, { x: 88, y: 220 }, { x: 90, y: 220 }, { x: 92, y: 219 }, { x: 95, y: 220 }, { x: 97, y: 219 }, { x: 99, y: 220 }, { x: 102, y: 218 }, { x: 105, y: 217 }, { x: 107, y: 216 }, { x: 110, y: 216 }, { x: 113, y: 214 }, { x: 116, y: 212 }, { x: 118, y: 210 }, { x: 121, y: 208 }, { x: 124, y: 205 }, { x: 126, y: 202 }, { x: 129, y: 199 }, { x: 132, y: 196 }, { x: 136, y: 191 }, { x: 139, y: 187 }, { x: 142, y: 182 }, { x: 144, y: 179 }, { x: 146, y: 174 }, { x: 148, y: 170 }, { x: 149, y: 168 }, { x: 151, y: 162 }, { x: 152, y: 160 }, { x: 152, y: 157 }, { x: 152, y: 155 }, { x: 152, y: 151 }, { x: 152, y: 149 }, { x: 152, y: 146 }, { x: 149, y: 142 }, { x: 148, y: 139 }, { x: 145, y: 137 }, { x: 141, y: 135 }, { x: 139, y: 135 }, { x: 134, y: 136 }, { x: 130, y: 140 }, { x: 128, y: 142 }, { x: 126, y: 145 }, { x: 122, y: 150 }, { x: 119, y: 158 }, { x: 117, y: 163 }, { x: 115, y: 170 }, { x: 114, y: 175 }, { x: 117, y: 184 }, { x: 120, y: 190 }, { x: 125, y: 199 }, { x: 129, y: 203 }, { x: 133, y: 208 }, { x: 138, y: 213 }, { x: 145, y: 215 }, { x: 155, y: 218 }, { x: 164, y: 219 }, { x: 166, y: 219 }, { x: 177, y: 219 }, { x: 182, y: 218 }, { x: 192, y: 216 }, { x: 196, y: 213 }, { x: 199, y: 212 }, { x: 201, y: 211 }, ], }, ] export const ALT_GESTURES: Gesture[] = [ { name: "circle layout", onComplete(editor, gesture?: TLDrawShape) { const bounds = editor.getShapePageBounds(gesture?.id!) const center = bounds?.center const radius = Math.max(bounds?.width || 0, bounds?.height || 0) / 2 const selected = editor.getSelectedShapes() const radialShapes = circleDistribution(editor, selected, center!, radius) editor.animateShapes(radialShapes, { animation: { duration: 600, easing: (t) => t * t * (3 - 2 * t), }, }) }, points: [ { x: 127, y: 141 }, { x: 124, y: 140 }, { x: 120, y: 139 }, { x: 118, y: 139 }, { x: 116, y: 139 }, { x: 111, y: 140 }, { x: 109, y: 141 }, { x: 104, y: 144 }, { x: 100, y: 147 }, { x: 96, y: 152 }, { x: 93, y: 157 }, { x: 90, y: 163 }, { x: 87, y: 169 }, { x: 85, y: 175 }, { x: 83, y: 181 }, { x: 82, y: 190 }, { x: 82, y: 195 }, { x: 83, y: 200 }, { x: 84, y: 205 }, { x: 88, y: 213 }, { x: 91, y: 216 }, { x: 96, y: 219 }, { x: 103, y: 222 }, { x: 108, y: 224 }, { x: 111, y: 224 }, { x: 120, y: 224 }, { x: 133, y: 223 }, { x: 142, y: 222 }, { x: 152, y: 218 }, { x: 160, y: 214 }, { x: 167, y: 210 }, { x: 173, y: 204 }, { x: 178, y: 198 }, { x: 179, y: 196 }, { x: 182, y: 188 }, { x: 182, y: 177 }, { x: 178, y: 167 }, { x: 170, y: 150 }, { x: 163, y: 138 }, { x: 152, y: 130 }, { x: 143, y: 129 }, { x: 140, y: 131 }, { x: 129, y: 136 }, { x: 126, y: 139 }, ], }, { name: "triangle layout", onComplete(editor, gesture?: TLDrawShape) { const bounds = editor.getShapePageBounds(gesture?.id!) const center = bounds?.center const radius = Math.max(bounds?.width || 0, bounds?.height || 0) / 2 const selected = editor.getSelectedShapes() const radialShapes = triangleDistribution(editor, selected, center!, radius) editor.animateShapes(radialShapes, { animation: { duration: 600, easing: (t) => t * t * (3 - 2 * t), }, }) }, points: [ { x: 137, y: 139 }, { x: 135, y: 141 }, { x: 133, y: 144 }, { x: 132, y: 146 }, { x: 130, y: 149 }, { x: 128, y: 151 }, { x: 126, y: 155 }, { x: 123, y: 160 }, { x: 120, y: 166 }, { x: 116, y: 171 }, { x: 112, y: 177 }, { x: 107, y: 183 }, { x: 102, y: 188 }, { x: 100, y: 191 }, { x: 95, y: 195 }, { x: 90, y: 199 }, { x: 86, y: 203 }, { x: 82, y: 206 }, { x: 80, y: 209 }, { x: 75, y: 213 }, { x: 73, y: 213 }, { x: 70, y: 216 }, { x: 67, y: 219 }, { x: 64, y: 221 }, { x: 61, y: 223 }, { x: 60, y: 225 }, { x: 62, y: 226 }, { x: 65, y: 225 }, { x: 67, y: 226 }, { x: 74, y: 226 }, { x: 77, y: 227 }, { x: 85, y: 229 }, { x: 91, y: 230 }, { x: 99, y: 231 }, { x: 108, y: 232 }, { x: 116, y: 233 }, { x: 125, y: 233 }, { x: 134, y: 234 }, { x: 145, y: 233 }, { x: 153, y: 232 }, { x: 160, y: 233 }, { x: 170, y: 234 }, { x: 177, y: 235 }, { x: 179, y: 236 }, { x: 186, y: 237 }, { x: 193, y: 238 }, { x: 198, y: 239 }, { x: 200, y: 237 }, { x: 202, y: 239 }, { x: 204, y: 238 }, { x: 206, y: 234 }, { x: 205, y: 230 }, { x: 202, y: 222 }, { x: 197, y: 216 }, { x: 192, y: 207 }, { x: 186, y: 198 }, { x: 179, y: 189 }, { x: 174, y: 183 }, { x: 170, y: 178 }, { x: 164, y: 171 }, { x: 161, y: 168 }, { x: 154, y: 160 }, { x: 148, y: 155 }, { x: 143, y: 150 }, { x: 138, y: 148 }, { x: 136, y: 148 }, ], }, ]