|
97 | 97 | <select
|
98 | 98 | @input="updateTimingFunction"
|
99 | 99 | @change="
|
100 |
| - (e) => { |
101 |
| - cubicBezierValues = JSON.parse( |
102 |
| - JSON.stringify( |
103 |
| - bezierPresets[ |
104 |
| - (e.target as HTMLInputElement).value |
105 |
| - ], |
106 |
| - ), |
107 |
| - ); |
108 |
| - updateTimingFunction(); |
109 |
| - } |
| 100 | + (e) => |
| 101 | + updateCubicBezierPreset( |
| 102 | + (e.target as HTMLSelectElement).value, |
| 103 | + ) |
110 | 104 | "
|
111 | 105 | >
|
112 | 106 | <option
|
|
119 | 113 |
|
120 | 114 | <svg
|
121 | 115 | ref="svgCubicBezierEl"
|
122 |
| - viewBox="-0.125 -1.125 1.25 1.25" |
| 116 | + viewBox="0 -1.5 1 2" |
123 | 117 | xmlns="http://www.w3.org/2000/svg"
|
124 |
| - ></svg> |
| 118 | + @mousedown="startCubicBezierDragging" |
| 119 | + @mousemove="cubicBezierDrag" |
| 120 | + @mouseup="stopCubicBezierDragging" |
| 121 | + @mouseleave="stopCubicBezierDragging" |
| 122 | + > |
| 123 | + <g ref="cubicBezierPathEl"></g> |
| 124 | + <circle |
| 125 | + v-for="(point, index) in controlPoints" |
| 126 | + :key="index" |
| 127 | + :cx="point.x" |
| 128 | + :cy="point.y" |
| 129 | + :data-index="index" |
| 130 | + @mouseover=" |
| 131 | + (e) => { |
| 132 | + (e.target as HTMLElement).style.setProperty( |
| 133 | + '--stroke-width', |
| 134 | + '0.15', |
| 135 | + ); |
| 136 | + } |
| 137 | + " |
| 138 | + @mouseleave=" |
| 139 | + (e) => { |
| 140 | + (e.target as HTMLElement).style.setProperty( |
| 141 | + '--stroke-width', |
| 142 | + '0.1', |
| 143 | + ); |
| 144 | + } |
| 145 | + " |
| 146 | + /> |
| 147 | + </svg> |
125 | 148 |
|
126 | 149 | <label class="preset-label" @click="copyToClipboard"
|
127 | 150 | >{{ cubicBezierPreset }}
|
128 |
| - <div ref="copyTextEl" class="info">Copied!</div> |
129 |
| - <font-awesome-icon :icon="['fas', 'clipboard']" |
130 |
| - /></label> |
131 |
| - |
132 |
| - <input |
133 |
| - @input="updateTimingFunction" |
134 |
| - v-model.number="cubicBezierValues[0]" |
135 |
| - type="range" |
136 |
| - :min="-2" |
137 |
| - :max="2" |
138 |
| - step="0.01" |
139 |
| - /> |
140 |
| - <input |
141 |
| - @input="updateTimingFunction" |
142 |
| - v-model.number="cubicBezierValues[1]" |
143 |
| - type="range" |
144 |
| - :min="-2" |
145 |
| - :max="2" |
146 |
| - step="0.01" |
147 |
| - /> |
148 |
| - <input |
149 |
| - @input="updateTimingFunction" |
150 |
| - v-model.number="cubicBezierValues[2]" |
151 |
| - type="range" |
152 |
| - :min="-2" |
153 |
| - :max="2" |
154 |
| - step="0.01" |
155 |
| - /> |
156 |
| - <input |
157 |
| - @input="updateTimingFunction" |
158 |
| - v-model.number="cubicBezierValues[3]" |
159 |
| - type="range" |
160 |
| - :min="-2" |
161 |
| - :max="2" |
162 |
| - step="0.01" |
163 |
| - /> |
| 151 | + </label> |
164 | 152 | </div>
|
165 | 153 | </template>
|
166 | 154 | </div>
|
@@ -326,27 +314,104 @@ let cubicBezierValues = $ref(
|
326 | 314 | bezierPresets[cubicBezierPreset] as [number, number, number, number],
|
327 | 315 | );
|
328 | 316 | const svgCubicBezierEl = $ref(null);
|
| 317 | +const cubicBezierPathEl = $ref(null); |
| 318 | +
|
| 319 | +let isDragging = $ref(false); |
| 320 | +let currentPointIndex = $ref(null); |
| 321 | +let controlPoints = $ref([ |
| 322 | + { x: 0.01, y: 0 }, |
| 323 | + { x: cubicBezierValues[0], y: cubicBezierValues[1] }, |
| 324 | + { x: cubicBezierValues[2], y: cubicBezierValues[3] }, |
| 325 | + { x: 0.97, y: 1 }, |
| 326 | +]); |
| 327 | +
|
| 328 | +const bezierPath = computed(() => { |
| 329 | + const scaledValues = cubicBezierValues.map((v) => { |
| 330 | + return v; |
| 331 | + }); |
| 332 | +
|
| 333 | + return svgCubicBezier(...(scaledValues as [number, number, number, number])); |
| 334 | +}); |
| 335 | +
|
| 336 | +const startCubicBezierDragging = (event: MouseEvent) => { |
| 337 | + const target = (event.target as SVGElement).closest("circle"); |
| 338 | +
|
| 339 | + if (target) { |
| 340 | + isDragging = true; |
| 341 | + currentPointIndex = parseInt(target.getAttribute("data-index")); |
| 342 | + } |
| 343 | +}; |
| 344 | +
|
| 345 | +const stopCubicBezierDragging = () => { |
| 346 | + isDragging = false; |
| 347 | + currentPointIndex = null; |
| 348 | +}; |
| 349 | +
|
| 350 | +let scaleCubicBezierValues = $ref(false); |
| 351 | +
|
| 352 | +const cubicBezierDrag = (event: MouseEvent) => { |
| 353 | + if (isDragging && currentPointIndex !== null) { |
| 354 | + // if the current point is a boundary, exit: |
| 355 | + if (currentPointIndex === 0 || currentPointIndex === 3) { |
| 356 | + return; |
| 357 | + } |
| 358 | +
|
| 359 | + const svgRect = cubicBezierPathEl.getBoundingClientRect(); |
| 360 | +
|
| 361 | + const { width, height, left, top } = svgRect; |
| 362 | +
|
| 363 | + const x = (event.clientX - left) / width; |
| 364 | + const y = 1 - (event.clientY - top) / height; |
| 365 | +
|
| 366 | + // Update the control point position |
| 367 | + controlPoints[currentPointIndex] = { |
| 368 | + x, |
| 369 | + y, |
| 370 | + }; |
| 371 | +
|
| 372 | + // Update cubicBezierValues |
| 373 | + cubicBezierValues = [ |
| 374 | + controlPoints[1].x, |
| 375 | + controlPoints[1].y, |
| 376 | + controlPoints[2].x, |
| 377 | + controlPoints[2].y, |
| 378 | + ]; |
| 379 | +
|
| 380 | + scaleCubicBezierValues = true; |
| 381 | + updateTimingFunction(); |
| 382 | + } |
| 383 | +}; |
| 384 | +
|
| 385 | +const updateCubicBezierPreset = (preset: string) => { |
| 386 | + cubicBezierPreset = preset; |
| 387 | + cubicBezierValues = JSON.parse(JSON.stringify(bezierPresets[preset])); |
| 388 | +
|
| 389 | + // update the control points |
| 390 | + controlPoints[1] = { x: cubicBezierValues[0], y: cubicBezierValues[1] }; |
| 391 | + controlPoints[2] = { x: cubicBezierValues[2], y: cubicBezierValues[3] }; |
| 392 | +
|
| 393 | + scaleCubicBezierValues = true; |
| 394 | + updateTimingFunction(); |
| 395 | +}; |
329 | 396 |
|
330 | 397 | const updateTimingFunction = () => {
|
331 | 398 | let timingFunction = timingFunctions[timingFunctionKey];
|
| 399 | +
|
332 | 400 | if (timingFunctionKey === "steps") {
|
333 | 401 | timingFunction = timingFunctions[timingFunctionKey](steps, jumpTerm);
|
334 | 402 | } else if (timingFunctionKey === "cubicBezier") {
|
| 403 | + const scaledValues = cubicBezierValues.map((v) => { |
| 404 | + return v; |
| 405 | + }); |
| 406 | +
|
335 | 407 | timingFunction = CSSBezier(
|
336 |
| - ...(cubicBezierValues as [number, number, number, number]), |
| 408 | + ...(scaledValues as [number, number, number, number]), |
337 | 409 | );
|
338 | 410 | cubicBezierPreset = `cubic-bezier(${cubicBezierValues
|
339 | 411 | .map((v) => v.toFixed(2))
|
340 | 412 | .join(",")})`;
|
341 | 413 |
|
342 |
| - const path = svgCubicBezier( |
343 |
| - cubicBezierValues[0], |
344 |
| - cubicBezierValues[1], |
345 |
| - cubicBezierValues[2], |
346 |
| - cubicBezierValues[3], |
347 |
| - ); |
348 |
| -
|
349 |
| - svgCubicBezierEl.innerHTML = path; |
| 414 | + cubicBezierPathEl.innerHTML = bezierPath.value; |
350 | 415 | }
|
351 | 416 |
|
352 | 417 | setTimingFunction(timingFunction);
|
@@ -482,6 +547,7 @@ const fadeInOut = (el) => {
|
482 | 547 | };
|
483 | 548 |
|
484 | 549 | let copyTextEl = $ref(null);
|
| 550 | +
|
485 | 551 | const copyToClipboard = async () => {
|
486 | 552 | navigator.clipboard.writeText(await cssKeyframesString.value);
|
487 | 553 | fadeInOut(copyTextEl);
|
@@ -552,51 +618,58 @@ input[type="range"] {
|
552 | 618 |
|
553 | 619 | .cubic-bezier-controls {
|
554 | 620 | grid-column: span 2;
|
| 621 | +
|
555 | 622 | display: grid;
|
556 |
| - gap: 1rem 0.5rem; |
557 |
| - grid-template-columns: auto auto; |
| 623 | +
|
| 624 | + grid-template-columns: 25% 75%; |
558 | 625 | align-items: center;
|
559 | 626 |
|
| 627 | + select { |
| 628 | + width: 100%; |
| 629 | + } |
| 630 | +
|
560 | 631 | label {
|
561 | 632 | overflow: hidden;
|
562 | 633 | white-space: pre;
|
563 | 634 | grid-column: span 2;
|
564 | 635 | max-width: 100%;
|
565 | 636 | }
|
566 | 637 |
|
567 |
| - input { |
568 |
| - grid-column: span 2; |
569 |
| - margin: 0; |
570 |
| -
|
571 |
| - background: linear-gradient( |
572 |
| - to right, |
573 |
| - #f00 0%, |
574 |
| - #ff0 17%, |
575 |
| - #0f0 33%, |
576 |
| - #0ff 50%, |
577 |
| - #00f 67%, |
578 |
| - #f0f 83%, |
579 |
| - #f00 100% |
580 |
| - ) !important; |
581 |
| - } |
582 |
| -
|
583 | 638 | svg::v-deep {
|
584 |
| - width: 200px; |
| 639 | + width: 100%; |
| 640 | +
|
585 | 641 | aspect-ratio: 1 / 1;
|
586 |
| - --stroke-width: 0.07; |
| 642 | + --stroke-width: 0.1; |
| 643 | + --circle-color: rgb(226, 61, 61); |
| 644 | + --path-color: rgb(137, 20, 239); |
| 645 | +
|
| 646 | + circle { |
| 647 | + r: calc(var(--stroke-width) / 2); |
| 648 | + stroke: var(--circle-color); |
| 649 | + fill: var(--circle-color); |
| 650 | + stroke-width: 0; |
| 651 | +
|
| 652 | + cursor: move; |
| 653 | + } |
| 654 | +
|
| 655 | + circle:nth-child(5), |
| 656 | + circle:nth-child(2) { |
| 657 | + --circle-color: var(--path-color); |
| 658 | + cursor: not-allowed; |
| 659 | + } |
587 | 660 |
|
588 | 661 | g {
|
589 |
| - circle { |
590 |
| - r: calc(var(--stroke-width) / 2); |
591 |
| - stroke: black; |
592 |
| - stroke-width: 0; |
593 |
| - } |
594 | 662 | path {
|
595 |
| - stroke: rgb(93, 246, 220); |
| 663 | + stroke: rgb(137, 20, 239); |
596 | 664 | stroke-width: var(--stroke-width);
|
597 | 665 | fill: none;
|
598 | 666 | }
|
599 | 667 | }
|
| 668 | +
|
| 669 | + > * { |
| 670 | + --scale: 1; |
| 671 | + transform: scale(var(--scale), calc(-1 * var(--scale))); |
| 672 | + } |
600 | 673 | }
|
601 | 674 | }
|
602 | 675 |
|
|
0 commit comments