@rollstack/rscanvas 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3503,31 +3503,149 @@ var PanControl = class {
3503
3503
  };
3504
3504
 
3505
3505
  // src/controls/drag_control.ts
3506
+ var ALIGN_THRESHOLD = 5;
3507
+ var GUIDE_COLOR = "#4a90d9";
3508
+ var SNAP_COLOR = "#e06c75";
3506
3509
  var DragControl = class {
3507
3510
  constructor(canvas) {
3508
- this._objectId = null;
3509
3511
  this._lastViewX = 0;
3510
3512
  this._lastViewY = 0;
3513
+ this._guideX = null;
3514
+ this._guideY = null;
3515
+ this._guideRect = null;
3516
+ this._snappedX = false;
3517
+ this._snappedY = false;
3518
+ this._guideXEl = null;
3519
+ this._guideYEl = null;
3520
+ this._guideRectEl = null;
3521
+ this._bestDx = Infinity;
3522
+ this._bestDy = Infinity;
3523
+ this._node = null;
3524
+ this._candidateRects = [];
3525
+ this._visitObjectForSnap = (other) => {
3526
+ if (!this._node || other.getId() === this._node.getId()) {
3527
+ return;
3528
+ }
3529
+ for (const xe of [other.getX(), other.getX() + other.getWidth()]) {
3530
+ for (const de of [
3531
+ this._node.getX() - xe,
3532
+ this._node.getX() + this._node.getWidth() - xe
3533
+ ]) {
3534
+ if (Math.abs(de) < ALIGN_THRESHOLD && Math.abs(de) < Math.abs(this._bestDx)) {
3535
+ this._bestDx = de;
3536
+ }
3537
+ }
3538
+ }
3539
+ for (const ye of [other.getY(), other.getY() + other.getHeight()]) {
3540
+ for (const de of [
3541
+ this._node.getY() - ye,
3542
+ this._node.getY() + this._node.getHeight() - ye
3543
+ ]) {
3544
+ if (Math.abs(de) < ALIGN_THRESHOLD && Math.abs(de) < Math.abs(this._bestDy)) {
3545
+ this._bestDy = de;
3546
+ }
3547
+ }
3548
+ }
3549
+ };
3550
+ this._visitObjectForGuides = (other) => {
3551
+ if (!this._node || other.getId() === this._node.getId()) {
3552
+ return;
3553
+ }
3554
+ const dragCx = this._node.getX() + this._node.getWidth() / 2;
3555
+ const dragCy = this._node.getY() + this._node.getHeight() / 2;
3556
+ let matched = false;
3557
+ for (const xe of [other.getX(), other.getX() + other.getWidth()]) {
3558
+ if (Math.abs(this._node.getX() - xe) < ALIGN_THRESHOLD || Math.abs(this._node.getX() + this._node.getWidth() - xe) < ALIGN_THRESHOLD) {
3559
+ if (this._guideX === null) {
3560
+ this._guideX = xe;
3561
+ }
3562
+ matched = true;
3563
+ }
3564
+ }
3565
+ for (const ye of [other.getY(), other.getY() + other.getHeight()]) {
3566
+ if (Math.abs(this._node.getY() - ye) < ALIGN_THRESHOLD || Math.abs(this._node.getY() + this._node.getHeight() - ye) < ALIGN_THRESHOLD) {
3567
+ if (this._guideY === null) {
3568
+ this._guideY = ye;
3569
+ }
3570
+ matched = true;
3571
+ }
3572
+ }
3573
+ if (matched) {
3574
+ const distSq = (dragCx - (other.getX() + other.getWidth() / 2)) ** 2 + (dragCy - (other.getY() + other.getHeight() / 2)) ** 2;
3575
+ this._candidateRects.push({
3576
+ rect: {
3577
+ x: other.getX(),
3578
+ y: other.getY(),
3579
+ w: other.getWidth(),
3580
+ h: other.getHeight()
3581
+ },
3582
+ distSq
3583
+ });
3584
+ }
3585
+ };
3511
3586
  this._canvas = canvas;
3512
3587
  }
3588
+ _updateGuidesAndSnap() {
3589
+ if (!this._node) {
3590
+ return;
3591
+ }
3592
+ this._bestDx = Infinity;
3593
+ this._bestDy = Infinity;
3594
+ this._guideX = null;
3595
+ this._guideY = null;
3596
+ this._guideRect = null;
3597
+ this._snappedX = false;
3598
+ this._snappedY = false;
3599
+ this._canvas.forEachObject(this._visitObjectForSnap);
3600
+ const snapX = Math.abs(this._bestDx) < ALIGN_THRESHOLD ? -this._bestDx : 0;
3601
+ const snapY = Math.abs(this._bestDy) < ALIGN_THRESHOLD ? -this._bestDy : 0;
3602
+ this._snappedX = snapX !== 0;
3603
+ this._snappedY = snapY !== 0;
3604
+ if (snapX !== 0 || snapY !== 0) {
3605
+ this._canvas.translateObject(this._node.getId(), snapX, snapY);
3606
+ }
3607
+ this._candidateRects.length = 0;
3608
+ this._canvas.forEachObject(this._visitObjectForGuides);
3609
+ let bestRect = null;
3610
+ for (const c of this._candidateRects) {
3611
+ if (bestRect === null || c.distSq < bestRect.distSq) {
3612
+ bestRect = c;
3613
+ }
3614
+ }
3615
+ this._guideRect = bestRect !== null ? bestRect.rect : null;
3616
+ }
3617
+ _clearGuides() {
3618
+ this._node = null;
3619
+ this._guideX = null;
3620
+ this._guideY = null;
3621
+ this._guideRect = null;
3622
+ this._snappedX = false;
3623
+ this._snappedY = false;
3624
+ }
3513
3625
  handleEvent(event) {
3514
3626
  if (event.type === "mousedown") {
3515
- const objectId = this._canvas.objectIdAtXY(event.viewX ?? 0, event.viewY ?? 0);
3627
+ const objectId = this._canvas.objectIdAtXY(
3628
+ event.viewX ?? 0,
3629
+ event.viewY ?? 0
3630
+ );
3516
3631
  if (!objectId) {
3517
3632
  return null;
3518
3633
  }
3519
- this._objectId = objectId;
3634
+ this._node = this._canvas.getObjectNode(objectId);
3635
+ if (!this._node) {
3636
+ return null;
3637
+ }
3520
3638
  this._lastViewX = event.viewX ?? 0;
3521
3639
  this._lastViewY = event.viewY ?? 0;
3522
3640
  this._canvas.changeCursor("grabbing");
3523
3641
  return this;
3524
3642
  }
3525
3643
  if (event.type === "mouseup" || event.type === "mouseleave") {
3526
- this._objectId = null;
3644
+ this._clearGuides();
3527
3645
  this._canvas.changeCursor("default");
3528
3646
  return null;
3529
3647
  }
3530
- if (event.type === "mousemove" && this._objectId) {
3648
+ if (event.type === "mousemove" && this._node) {
3531
3649
  const viewX = event.viewX ?? 0;
3532
3650
  const viewY = event.viewY ?? 0;
3533
3651
  const scale = this._canvas.getScale();
@@ -3535,24 +3653,79 @@ var DragControl = class {
3535
3653
  const dy = (viewY - this._lastViewY) / scale;
3536
3654
  this._lastViewX = viewX;
3537
3655
  this._lastViewY = viewY;
3538
- this._canvas.translateObject(this._objectId, dx, dy);
3656
+ this._canvas.translateObject(this._node.getId(), dx, dy);
3657
+ this._updateGuidesAndSnap();
3539
3658
  this._canvas.changeCursor("grabbing");
3540
3659
  return this;
3541
3660
  }
3542
3661
  if (event.type === "mousemove") {
3543
- if (this._canvas.objectIdAtLowerRightCorner(event.viewX ?? 0, event.viewY ?? 0) || this._canvas.objectIdAtUpperRightCorner(event.viewX ?? 0, event.viewY ?? 0)) {
3662
+ if (this._canvas.objectIdAtLowerRightCorner(
3663
+ event.viewX ?? 0,
3664
+ event.viewY ?? 0
3665
+ ) || this._canvas.objectIdAtUpperRightCorner(
3666
+ event.viewX ?? 0,
3667
+ event.viewY ?? 0
3668
+ )) {
3544
3669
  return null;
3545
3670
  }
3546
- const objectId = this._canvas.objectIdAtXY(event.viewX ?? 0, event.viewY ?? 0);
3671
+ const objectId = this._canvas.objectIdAtXY(
3672
+ event.viewX ?? 0,
3673
+ event.viewY ?? 0
3674
+ );
3547
3675
  if (objectId) {
3548
3676
  this._canvas.changeCursor("grab");
3549
3677
  }
3550
3678
  }
3551
3679
  return null;
3552
3680
  }
3553
- render(_node) {
3681
+ render(node) {
3682
+ if (this._guideX !== null) {
3683
+ if (!this._guideXEl) {
3684
+ this._guideXEl = document.createElement("div");
3685
+ node.appendChild(this._guideXEl);
3686
+ }
3687
+ const xColor = this._snappedX ? SNAP_COLOR : GUIDE_COLOR;
3688
+ this._guideXEl.style.cssText = `position:absolute;left:${this._guideX}px;top:0;width:1px;height:${this._canvas.getCanvasHeight()}px;background:${xColor};pointer-events:none`;
3689
+ } else if (this._guideXEl) {
3690
+ this._guideXEl.remove();
3691
+ this._guideXEl = null;
3692
+ }
3693
+ if (this._guideY !== null) {
3694
+ if (!this._guideYEl) {
3695
+ this._guideYEl = document.createElement("div");
3696
+ node.appendChild(this._guideYEl);
3697
+ }
3698
+ const yColor = this._snappedY ? SNAP_COLOR : GUIDE_COLOR;
3699
+ this._guideYEl.style.cssText = `position:absolute;left:0;top:${this._guideY}px;width:${this._canvas.getCanvasWidth()}px;height:1px;background:${yColor};pointer-events:none`;
3700
+ } else if (this._guideYEl) {
3701
+ this._guideYEl.remove();
3702
+ this._guideYEl = null;
3703
+ }
3704
+ if (this._guideRect !== null) {
3705
+ const r = this._guideRect;
3706
+ if (!this._guideRectEl) {
3707
+ this._guideRectEl = document.createElement("div");
3708
+ node.appendChild(this._guideRectEl);
3709
+ }
3710
+ this._guideRectEl.style.cssText = `position:absolute;left:${r.x}px;top:${r.y}px;width:${r.w}px;height:${r.h}px;border:1px solid ${GUIDE_COLOR};pointer-events:none;box-sizing:border-box`;
3711
+ } else if (this._guideRectEl) {
3712
+ this._guideRectEl.remove();
3713
+ this._guideRectEl = null;
3714
+ }
3554
3715
  }
3555
3716
  destroy() {
3717
+ if (this._guideXEl) {
3718
+ this._guideXEl.remove();
3719
+ this._guideXEl = null;
3720
+ }
3721
+ if (this._guideYEl) {
3722
+ this._guideYEl.remove();
3723
+ this._guideYEl = null;
3724
+ }
3725
+ if (this._guideRectEl) {
3726
+ this._guideRectEl.remove();
3727
+ this._guideRectEl = null;
3728
+ }
3556
3729
  }
3557
3730
  };
3558
3731
 
@@ -3680,7 +3853,7 @@ var DeleteControl = class {
3680
3853
  };
3681
3854
 
3682
3855
  // src/controls/keyboard_move_control.ts
3683
- var ARROW_KEY_STEP = 8;
3856
+ var ARROW_KEY_STEP_VIEW = 2;
3684
3857
  var KeyboardMoveControl = class {
3685
3858
  constructor(canvas) {
3686
3859
  this._canvas = canvas;
@@ -3693,16 +3866,17 @@ var KeyboardMoveControl = class {
3693
3866
  if (!id) {
3694
3867
  return null;
3695
3868
  }
3869
+ const step = this._canvas.toWorldSize(ARROW_KEY_STEP_VIEW);
3696
3870
  let dx = 0;
3697
3871
  let dy = 0;
3698
3872
  if (event.key === "ArrowLeft") {
3699
- dx = -ARROW_KEY_STEP;
3873
+ dx = -step;
3700
3874
  } else if (event.key === "ArrowRight") {
3701
- dx = ARROW_KEY_STEP;
3875
+ dx = step;
3702
3876
  } else if (event.key === "ArrowUp") {
3703
- dy = -ARROW_KEY_STEP;
3877
+ dy = -step;
3704
3878
  } else if (event.key === "ArrowDown") {
3705
- dy = ARROW_KEY_STEP;
3879
+ dy = step;
3706
3880
  } else {
3707
3881
  return null;
3708
3882
  }
@@ -3776,6 +3950,8 @@ var RSCanvas = class {
3776
3950
  this._translateX = 0;
3777
3951
  this._translateY = 0;
3778
3952
  this._objects = {};
3953
+ this._slides = [{ id: "default", objects: {} }];
3954
+ this._selectedSlideId = "default";
3779
3955
  this._objectConstructors = /* @__PURE__ */ new Map();
3780
3956
  this._objectEditorConstructors = /* @__PURE__ */ new Map();
3781
3957
  this._editingObjectId = null;
@@ -3792,6 +3968,25 @@ var RSCanvas = class {
3792
3968
  this._cornerPosScratch = { worldX: 0, worldY: 0 };
3793
3969
  this._boundingClientRect = { left: 0, top: 0 };
3794
3970
  this._controls = [];
3971
+ this._onPaint = () => {
3972
+ this._rafId = null;
3973
+ if (this._pendingState) {
3974
+ if (this._pendingStateVersion >= this._internalStateVersion) {
3975
+ this._updateObjectTree(this._pendingState);
3976
+ }
3977
+ this._pendingState = null;
3978
+ }
3979
+ if (this._dirtyTransform) {
3980
+ this._dirtyTransform = false;
3981
+ this._innerContainerNode.style.transform = `translate(${this._translateX}px, ${this._translateY}px) scale(${this._scale})`;
3982
+ }
3983
+ for (const id in this._objects) {
3984
+ this._objects[id].render();
3985
+ }
3986
+ for (const ctrl of this._controls) {
3987
+ ctrl.render(this._overlayNode);
3988
+ }
3989
+ };
3795
3990
  this._onMouseDown = (e) => {
3796
3991
  if (this._destroyed || this._editingObjectId) {
3797
3992
  return;
@@ -3898,7 +4093,6 @@ var RSCanvas = class {
3898
4093
  this._dispatchToControls(event);
3899
4094
  };
3900
4095
  this._localInvalidate = () => this._invalidate();
3901
- this._onPaint = () => this._paint();
3902
4096
  this._containerNode = container;
3903
4097
  this._width = options?.width ?? 1200;
3904
4098
  this._height = options?.height ?? 800;
@@ -3995,25 +4189,6 @@ var RSCanvas = class {
3995
4189
  }
3996
4190
  }
3997
4191
  }
3998
- _paint() {
3999
- this._rafId = null;
4000
- if (this._pendingState) {
4001
- if (this._pendingStateVersion >= this._internalStateVersion) {
4002
- this._updateObjectTree(this._pendingState);
4003
- }
4004
- this._pendingState = null;
4005
- }
4006
- if (this._dirtyTransform) {
4007
- this._dirtyTransform = false;
4008
- this._innerContainerNode.style.transform = `translate(${this._translateX}px, ${this._translateY}px) scale(${this._scale})`;
4009
- }
4010
- for (const id in this._objects) {
4011
- this._objects[id].render();
4012
- }
4013
- for (const ctrl of this._controls) {
4014
- ctrl.render(this._overlayNode);
4015
- }
4016
- }
4017
4192
  _invalidate() {
4018
4193
  if (this._rafId != null) {
4019
4194
  return;
@@ -4101,6 +4276,14 @@ var RSCanvas = class {
4101
4276
  _getObjectEditorConstructor(objectType) {
4102
4277
  return this._objectEditorConstructors.get(objectType) ?? null;
4103
4278
  }
4279
+ _findSlideIndexById(slideId) {
4280
+ for (let i = 0; i < this._slides.length; i++) {
4281
+ if (this._slides[i].id === slideId) {
4282
+ return i;
4283
+ }
4284
+ }
4285
+ return -1;
4286
+ }
4104
4287
  _updateObjectTree(objects) {
4105
4288
  for (const id in this._objects) {
4106
4289
  if (!(id in objects)) {
@@ -4147,47 +4330,73 @@ var RSCanvas = class {
4147
4330
  this._objectEditorConstructors.set(objectType, constructor);
4148
4331
  }
4149
4332
  setState(state) {
4150
- this._pendingState = state.objects ?? {};
4333
+ this._slides = state.slides ?? [];
4334
+ if (this._slides.length === 0) {
4335
+ this._selectedSlideId = null;
4336
+ this._pendingState = {};
4337
+ } else {
4338
+ const idx = this._selectedSlideId !== null ? this._findSlideIndexById(this._selectedSlideId) : -1;
4339
+ if (idx < 0) {
4340
+ this._selectedSlideId = this._slides[0].id;
4341
+ }
4342
+ this._pendingState = this._slides[this._findSlideIndexById(this._selectedSlideId)]?.objects ?? {};
4343
+ }
4151
4344
  this._pendingStateVersion = this._internalStateVersion;
4152
4345
  this._invalidate();
4153
4346
  }
4154
4347
  getStateSFCT(state) {
4155
- if (!state.objects) {
4156
- state.objects = {};
4157
- }
4158
- for (const id in this._objects) {
4159
- const node = this._objects[id];
4160
- const cfg = state.objects[id];
4161
- if (cfg) {
4162
- cfg.type = node.getType();
4163
- cfg.x = node.getX();
4164
- cfg.y = node.getY();
4165
- cfg.width = node.getWidth();
4166
- cfg.height = node.getHeight();
4167
- cfg.rotation = node.getRotation();
4168
- if (!cfg.options) {
4169
- cfg.options = {};
4348
+ if (!state.slides) {
4349
+ state.slides = [];
4350
+ }
4351
+ state.slides.length = this._slides.length;
4352
+ const selectedIdx = this._selectedSlideId !== null ? this._findSlideIndexById(this._selectedSlideId) : -1;
4353
+ for (let i = 0; i < this._slides.length; i++) {
4354
+ if (i === selectedIdx) {
4355
+ let slide = state.slides[i];
4356
+ if (!slide) {
4357
+ slide = { id: this._slides[i].id, objects: {} };
4358
+ state.slides[i] = slide;
4359
+ }
4360
+ for (const id in this._objects) {
4361
+ const node = this._objects[id];
4362
+ let cfg = slide.objects[id];
4363
+ if (!cfg) {
4364
+ cfg = {};
4365
+ slide.objects[id] = cfg;
4366
+ }
4367
+ cfg.type = node.getType();
4368
+ cfg.x = node.getX();
4369
+ cfg.y = node.getY();
4370
+ cfg.width = node.getWidth();
4371
+ cfg.height = node.getHeight();
4372
+ cfg.rotation = node.getRotation();
4373
+ if (!cfg.options) {
4374
+ cfg.options = {};
4375
+ }
4376
+ node.getOptionsSFCT(cfg.options);
4377
+ }
4378
+ for (const id in slide.objects) {
4379
+ if (!(id in this._objects)) {
4380
+ delete slide.objects[id];
4381
+ }
4170
4382
  }
4171
- node.getOptionsSFCT(cfg.options);
4172
4383
  } else {
4173
- const options = {};
4174
- node.getOptionsSFCT(options);
4175
- state.objects[id] = {
4176
- type: node.getType(),
4177
- x: node.getX(),
4178
- y: node.getY(),
4179
- width: node.getWidth(),
4180
- height: node.getHeight(),
4181
- rotation: node.getRotation(),
4182
- options
4183
- };
4384
+ state.slides[i] = this._slides[i];
4184
4385
  }
4185
4386
  }
4186
- for (const id in state.objects) {
4187
- if (!(id in this._objects)) {
4188
- delete state.objects[id];
4189
- }
4387
+ }
4388
+ getSelectedSlideId() {
4389
+ return this._selectedSlideId;
4390
+ }
4391
+ setSelectedSlide(slideId) {
4392
+ const idx = this._findSlideIndexById(slideId);
4393
+ if (idx < 0 || slideId === this._selectedSlideId) {
4394
+ return;
4190
4395
  }
4396
+ this._selectedSlideId = slideId;
4397
+ this._pendingState = this._slides[idx]?.objects ?? {};
4398
+ this._pendingStateVersion = this._internalStateVersion;
4399
+ this._invalidate();
4191
4400
  }
4192
4401
  changeCursor(cursor) {
4193
4402
  this._containerNode.style.cursor = cursor;
@@ -4200,6 +4409,9 @@ var RSCanvas = class {
4200
4409
  getScale() {
4201
4410
  return this._scale;
4202
4411
  }
4412
+ toWorldSize(viewSize) {
4413
+ return viewSize / this._scale;
4414
+ }
4203
4415
  toWorldX(viewX) {
4204
4416
  return (viewX - this._translateX) / this._scale;
4205
4417
  }
@@ -4400,6 +4612,11 @@ var RSCanvas = class {
4400
4612
  this._emitStateChanged();
4401
4613
  this._invalidate();
4402
4614
  }
4615
+ forEachObject(fn) {
4616
+ for (const id in this._objects) {
4617
+ fn(this._objects[id]);
4618
+ }
4619
+ }
4403
4620
  async saveToImage(callback) {
4404
4621
  const clone = this._innerContainerNode.cloneNode(true);
4405
4622
  clone.style.transform = "none";