@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.js CHANGED
@@ -3540,31 +3540,149 @@ var PanControl = class {
3540
3540
  };
3541
3541
 
3542
3542
  // src/controls/drag_control.ts
3543
+ var ALIGN_THRESHOLD = 5;
3544
+ var GUIDE_COLOR = "#4a90d9";
3545
+ var SNAP_COLOR = "#e06c75";
3543
3546
  var DragControl = class {
3544
3547
  constructor(canvas) {
3545
- this._objectId = null;
3546
3548
  this._lastViewX = 0;
3547
3549
  this._lastViewY = 0;
3550
+ this._guideX = null;
3551
+ this._guideY = null;
3552
+ this._guideRect = null;
3553
+ this._snappedX = false;
3554
+ this._snappedY = false;
3555
+ this._guideXEl = null;
3556
+ this._guideYEl = null;
3557
+ this._guideRectEl = null;
3558
+ this._bestDx = Infinity;
3559
+ this._bestDy = Infinity;
3560
+ this._node = null;
3561
+ this._candidateRects = [];
3562
+ this._visitObjectForSnap = (other) => {
3563
+ if (!this._node || other.getId() === this._node.getId()) {
3564
+ return;
3565
+ }
3566
+ for (const xe of [other.getX(), other.getX() + other.getWidth()]) {
3567
+ for (const de of [
3568
+ this._node.getX() - xe,
3569
+ this._node.getX() + this._node.getWidth() - xe
3570
+ ]) {
3571
+ if (Math.abs(de) < ALIGN_THRESHOLD && Math.abs(de) < Math.abs(this._bestDx)) {
3572
+ this._bestDx = de;
3573
+ }
3574
+ }
3575
+ }
3576
+ for (const ye of [other.getY(), other.getY() + other.getHeight()]) {
3577
+ for (const de of [
3578
+ this._node.getY() - ye,
3579
+ this._node.getY() + this._node.getHeight() - ye
3580
+ ]) {
3581
+ if (Math.abs(de) < ALIGN_THRESHOLD && Math.abs(de) < Math.abs(this._bestDy)) {
3582
+ this._bestDy = de;
3583
+ }
3584
+ }
3585
+ }
3586
+ };
3587
+ this._visitObjectForGuides = (other) => {
3588
+ if (!this._node || other.getId() === this._node.getId()) {
3589
+ return;
3590
+ }
3591
+ const dragCx = this._node.getX() + this._node.getWidth() / 2;
3592
+ const dragCy = this._node.getY() + this._node.getHeight() / 2;
3593
+ let matched = false;
3594
+ for (const xe of [other.getX(), other.getX() + other.getWidth()]) {
3595
+ if (Math.abs(this._node.getX() - xe) < ALIGN_THRESHOLD || Math.abs(this._node.getX() + this._node.getWidth() - xe) < ALIGN_THRESHOLD) {
3596
+ if (this._guideX === null) {
3597
+ this._guideX = xe;
3598
+ }
3599
+ matched = true;
3600
+ }
3601
+ }
3602
+ for (const ye of [other.getY(), other.getY() + other.getHeight()]) {
3603
+ if (Math.abs(this._node.getY() - ye) < ALIGN_THRESHOLD || Math.abs(this._node.getY() + this._node.getHeight() - ye) < ALIGN_THRESHOLD) {
3604
+ if (this._guideY === null) {
3605
+ this._guideY = ye;
3606
+ }
3607
+ matched = true;
3608
+ }
3609
+ }
3610
+ if (matched) {
3611
+ const distSq = (dragCx - (other.getX() + other.getWidth() / 2)) ** 2 + (dragCy - (other.getY() + other.getHeight() / 2)) ** 2;
3612
+ this._candidateRects.push({
3613
+ rect: {
3614
+ x: other.getX(),
3615
+ y: other.getY(),
3616
+ w: other.getWidth(),
3617
+ h: other.getHeight()
3618
+ },
3619
+ distSq
3620
+ });
3621
+ }
3622
+ };
3548
3623
  this._canvas = canvas;
3549
3624
  }
3625
+ _updateGuidesAndSnap() {
3626
+ if (!this._node) {
3627
+ return;
3628
+ }
3629
+ this._bestDx = Infinity;
3630
+ this._bestDy = Infinity;
3631
+ this._guideX = null;
3632
+ this._guideY = null;
3633
+ this._guideRect = null;
3634
+ this._snappedX = false;
3635
+ this._snappedY = false;
3636
+ this._canvas.forEachObject(this._visitObjectForSnap);
3637
+ const snapX = Math.abs(this._bestDx) < ALIGN_THRESHOLD ? -this._bestDx : 0;
3638
+ const snapY = Math.abs(this._bestDy) < ALIGN_THRESHOLD ? -this._bestDy : 0;
3639
+ this._snappedX = snapX !== 0;
3640
+ this._snappedY = snapY !== 0;
3641
+ if (snapX !== 0 || snapY !== 0) {
3642
+ this._canvas.translateObject(this._node.getId(), snapX, snapY);
3643
+ }
3644
+ this._candidateRects.length = 0;
3645
+ this._canvas.forEachObject(this._visitObjectForGuides);
3646
+ let bestRect = null;
3647
+ for (const c of this._candidateRects) {
3648
+ if (bestRect === null || c.distSq < bestRect.distSq) {
3649
+ bestRect = c;
3650
+ }
3651
+ }
3652
+ this._guideRect = bestRect !== null ? bestRect.rect : null;
3653
+ }
3654
+ _clearGuides() {
3655
+ this._node = null;
3656
+ this._guideX = null;
3657
+ this._guideY = null;
3658
+ this._guideRect = null;
3659
+ this._snappedX = false;
3660
+ this._snappedY = false;
3661
+ }
3550
3662
  handleEvent(event) {
3551
3663
  if (event.type === "mousedown") {
3552
- const objectId = this._canvas.objectIdAtXY(event.viewX ?? 0, event.viewY ?? 0);
3664
+ const objectId = this._canvas.objectIdAtXY(
3665
+ event.viewX ?? 0,
3666
+ event.viewY ?? 0
3667
+ );
3553
3668
  if (!objectId) {
3554
3669
  return null;
3555
3670
  }
3556
- this._objectId = objectId;
3671
+ this._node = this._canvas.getObjectNode(objectId);
3672
+ if (!this._node) {
3673
+ return null;
3674
+ }
3557
3675
  this._lastViewX = event.viewX ?? 0;
3558
3676
  this._lastViewY = event.viewY ?? 0;
3559
3677
  this._canvas.changeCursor("grabbing");
3560
3678
  return this;
3561
3679
  }
3562
3680
  if (event.type === "mouseup" || event.type === "mouseleave") {
3563
- this._objectId = null;
3681
+ this._clearGuides();
3564
3682
  this._canvas.changeCursor("default");
3565
3683
  return null;
3566
3684
  }
3567
- if (event.type === "mousemove" && this._objectId) {
3685
+ if (event.type === "mousemove" && this._node) {
3568
3686
  const viewX = event.viewX ?? 0;
3569
3687
  const viewY = event.viewY ?? 0;
3570
3688
  const scale = this._canvas.getScale();
@@ -3572,24 +3690,79 @@ var DragControl = class {
3572
3690
  const dy = (viewY - this._lastViewY) / scale;
3573
3691
  this._lastViewX = viewX;
3574
3692
  this._lastViewY = viewY;
3575
- this._canvas.translateObject(this._objectId, dx, dy);
3693
+ this._canvas.translateObject(this._node.getId(), dx, dy);
3694
+ this._updateGuidesAndSnap();
3576
3695
  this._canvas.changeCursor("grabbing");
3577
3696
  return this;
3578
3697
  }
3579
3698
  if (event.type === "mousemove") {
3580
- if (this._canvas.objectIdAtLowerRightCorner(event.viewX ?? 0, event.viewY ?? 0) || this._canvas.objectIdAtUpperRightCorner(event.viewX ?? 0, event.viewY ?? 0)) {
3699
+ if (this._canvas.objectIdAtLowerRightCorner(
3700
+ event.viewX ?? 0,
3701
+ event.viewY ?? 0
3702
+ ) || this._canvas.objectIdAtUpperRightCorner(
3703
+ event.viewX ?? 0,
3704
+ event.viewY ?? 0
3705
+ )) {
3581
3706
  return null;
3582
3707
  }
3583
- const objectId = this._canvas.objectIdAtXY(event.viewX ?? 0, event.viewY ?? 0);
3708
+ const objectId = this._canvas.objectIdAtXY(
3709
+ event.viewX ?? 0,
3710
+ event.viewY ?? 0
3711
+ );
3584
3712
  if (objectId) {
3585
3713
  this._canvas.changeCursor("grab");
3586
3714
  }
3587
3715
  }
3588
3716
  return null;
3589
3717
  }
3590
- render(_node) {
3718
+ render(node) {
3719
+ if (this._guideX !== null) {
3720
+ if (!this._guideXEl) {
3721
+ this._guideXEl = document.createElement("div");
3722
+ node.appendChild(this._guideXEl);
3723
+ }
3724
+ const xColor = this._snappedX ? SNAP_COLOR : GUIDE_COLOR;
3725
+ this._guideXEl.style.cssText = `position:absolute;left:${this._guideX}px;top:0;width:1px;height:${this._canvas.getCanvasHeight()}px;background:${xColor};pointer-events:none`;
3726
+ } else if (this._guideXEl) {
3727
+ this._guideXEl.remove();
3728
+ this._guideXEl = null;
3729
+ }
3730
+ if (this._guideY !== null) {
3731
+ if (!this._guideYEl) {
3732
+ this._guideYEl = document.createElement("div");
3733
+ node.appendChild(this._guideYEl);
3734
+ }
3735
+ const yColor = this._snappedY ? SNAP_COLOR : GUIDE_COLOR;
3736
+ this._guideYEl.style.cssText = `position:absolute;left:0;top:${this._guideY}px;width:${this._canvas.getCanvasWidth()}px;height:1px;background:${yColor};pointer-events:none`;
3737
+ } else if (this._guideYEl) {
3738
+ this._guideYEl.remove();
3739
+ this._guideYEl = null;
3740
+ }
3741
+ if (this._guideRect !== null) {
3742
+ const r = this._guideRect;
3743
+ if (!this._guideRectEl) {
3744
+ this._guideRectEl = document.createElement("div");
3745
+ node.appendChild(this._guideRectEl);
3746
+ }
3747
+ 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`;
3748
+ } else if (this._guideRectEl) {
3749
+ this._guideRectEl.remove();
3750
+ this._guideRectEl = null;
3751
+ }
3591
3752
  }
3592
3753
  destroy() {
3754
+ if (this._guideXEl) {
3755
+ this._guideXEl.remove();
3756
+ this._guideXEl = null;
3757
+ }
3758
+ if (this._guideYEl) {
3759
+ this._guideYEl.remove();
3760
+ this._guideYEl = null;
3761
+ }
3762
+ if (this._guideRectEl) {
3763
+ this._guideRectEl.remove();
3764
+ this._guideRectEl = null;
3765
+ }
3593
3766
  }
3594
3767
  };
3595
3768
 
@@ -3717,7 +3890,7 @@ var DeleteControl = class {
3717
3890
  };
3718
3891
 
3719
3892
  // src/controls/keyboard_move_control.ts
3720
- var ARROW_KEY_STEP = 8;
3893
+ var ARROW_KEY_STEP_VIEW = 2;
3721
3894
  var KeyboardMoveControl = class {
3722
3895
  constructor(canvas) {
3723
3896
  this._canvas = canvas;
@@ -3730,16 +3903,17 @@ var KeyboardMoveControl = class {
3730
3903
  if (!id) {
3731
3904
  return null;
3732
3905
  }
3906
+ const step = this._canvas.toWorldSize(ARROW_KEY_STEP_VIEW);
3733
3907
  let dx = 0;
3734
3908
  let dy = 0;
3735
3909
  if (event.key === "ArrowLeft") {
3736
- dx = -ARROW_KEY_STEP;
3910
+ dx = -step;
3737
3911
  } else if (event.key === "ArrowRight") {
3738
- dx = ARROW_KEY_STEP;
3912
+ dx = step;
3739
3913
  } else if (event.key === "ArrowUp") {
3740
- dy = -ARROW_KEY_STEP;
3914
+ dy = -step;
3741
3915
  } else if (event.key === "ArrowDown") {
3742
- dy = ARROW_KEY_STEP;
3916
+ dy = step;
3743
3917
  } else {
3744
3918
  return null;
3745
3919
  }
@@ -3813,6 +3987,8 @@ var RSCanvas = class {
3813
3987
  this._translateX = 0;
3814
3988
  this._translateY = 0;
3815
3989
  this._objects = {};
3990
+ this._slides = [{ id: "default", objects: {} }];
3991
+ this._selectedSlideId = "default";
3816
3992
  this._objectConstructors = /* @__PURE__ */ new Map();
3817
3993
  this._objectEditorConstructors = /* @__PURE__ */ new Map();
3818
3994
  this._editingObjectId = null;
@@ -3829,6 +4005,25 @@ var RSCanvas = class {
3829
4005
  this._cornerPosScratch = { worldX: 0, worldY: 0 };
3830
4006
  this._boundingClientRect = { left: 0, top: 0 };
3831
4007
  this._controls = [];
4008
+ this._onPaint = () => {
4009
+ this._rafId = null;
4010
+ if (this._pendingState) {
4011
+ if (this._pendingStateVersion >= this._internalStateVersion) {
4012
+ this._updateObjectTree(this._pendingState);
4013
+ }
4014
+ this._pendingState = null;
4015
+ }
4016
+ if (this._dirtyTransform) {
4017
+ this._dirtyTransform = false;
4018
+ this._innerContainerNode.style.transform = `translate(${this._translateX}px, ${this._translateY}px) scale(${this._scale})`;
4019
+ }
4020
+ for (const id in this._objects) {
4021
+ this._objects[id].render();
4022
+ }
4023
+ for (const ctrl of this._controls) {
4024
+ ctrl.render(this._overlayNode);
4025
+ }
4026
+ };
3832
4027
  this._onMouseDown = (e) => {
3833
4028
  if (this._destroyed || this._editingObjectId) {
3834
4029
  return;
@@ -3935,7 +4130,6 @@ var RSCanvas = class {
3935
4130
  this._dispatchToControls(event);
3936
4131
  };
3937
4132
  this._localInvalidate = () => this._invalidate();
3938
- this._onPaint = () => this._paint();
3939
4133
  this._containerNode = container;
3940
4134
  this._width = options?.width ?? 1200;
3941
4135
  this._height = options?.height ?? 800;
@@ -4032,25 +4226,6 @@ var RSCanvas = class {
4032
4226
  }
4033
4227
  }
4034
4228
  }
4035
- _paint() {
4036
- this._rafId = null;
4037
- if (this._pendingState) {
4038
- if (this._pendingStateVersion >= this._internalStateVersion) {
4039
- this._updateObjectTree(this._pendingState);
4040
- }
4041
- this._pendingState = null;
4042
- }
4043
- if (this._dirtyTransform) {
4044
- this._dirtyTransform = false;
4045
- this._innerContainerNode.style.transform = `translate(${this._translateX}px, ${this._translateY}px) scale(${this._scale})`;
4046
- }
4047
- for (const id in this._objects) {
4048
- this._objects[id].render();
4049
- }
4050
- for (const ctrl of this._controls) {
4051
- ctrl.render(this._overlayNode);
4052
- }
4053
- }
4054
4229
  _invalidate() {
4055
4230
  if (this._rafId != null) {
4056
4231
  return;
@@ -4138,6 +4313,14 @@ var RSCanvas = class {
4138
4313
  _getObjectEditorConstructor(objectType) {
4139
4314
  return this._objectEditorConstructors.get(objectType) ?? null;
4140
4315
  }
4316
+ _findSlideIndexById(slideId) {
4317
+ for (let i = 0; i < this._slides.length; i++) {
4318
+ if (this._slides[i].id === slideId) {
4319
+ return i;
4320
+ }
4321
+ }
4322
+ return -1;
4323
+ }
4141
4324
  _updateObjectTree(objects) {
4142
4325
  for (const id in this._objects) {
4143
4326
  if (!(id in objects)) {
@@ -4184,47 +4367,73 @@ var RSCanvas = class {
4184
4367
  this._objectEditorConstructors.set(objectType, constructor);
4185
4368
  }
4186
4369
  setState(state) {
4187
- this._pendingState = state.objects ?? {};
4370
+ this._slides = state.slides ?? [];
4371
+ if (this._slides.length === 0) {
4372
+ this._selectedSlideId = null;
4373
+ this._pendingState = {};
4374
+ } else {
4375
+ const idx = this._selectedSlideId !== null ? this._findSlideIndexById(this._selectedSlideId) : -1;
4376
+ if (idx < 0) {
4377
+ this._selectedSlideId = this._slides[0].id;
4378
+ }
4379
+ this._pendingState = this._slides[this._findSlideIndexById(this._selectedSlideId)]?.objects ?? {};
4380
+ }
4188
4381
  this._pendingStateVersion = this._internalStateVersion;
4189
4382
  this._invalidate();
4190
4383
  }
4191
4384
  getStateSFCT(state) {
4192
- if (!state.objects) {
4193
- state.objects = {};
4194
- }
4195
- for (const id in this._objects) {
4196
- const node = this._objects[id];
4197
- const cfg = state.objects[id];
4198
- if (cfg) {
4199
- cfg.type = node.getType();
4200
- cfg.x = node.getX();
4201
- cfg.y = node.getY();
4202
- cfg.width = node.getWidth();
4203
- cfg.height = node.getHeight();
4204
- cfg.rotation = node.getRotation();
4205
- if (!cfg.options) {
4206
- cfg.options = {};
4385
+ if (!state.slides) {
4386
+ state.slides = [];
4387
+ }
4388
+ state.slides.length = this._slides.length;
4389
+ const selectedIdx = this._selectedSlideId !== null ? this._findSlideIndexById(this._selectedSlideId) : -1;
4390
+ for (let i = 0; i < this._slides.length; i++) {
4391
+ if (i === selectedIdx) {
4392
+ let slide = state.slides[i];
4393
+ if (!slide) {
4394
+ slide = { id: this._slides[i].id, objects: {} };
4395
+ state.slides[i] = slide;
4396
+ }
4397
+ for (const id in this._objects) {
4398
+ const node = this._objects[id];
4399
+ let cfg = slide.objects[id];
4400
+ if (!cfg) {
4401
+ cfg = {};
4402
+ slide.objects[id] = cfg;
4403
+ }
4404
+ cfg.type = node.getType();
4405
+ cfg.x = node.getX();
4406
+ cfg.y = node.getY();
4407
+ cfg.width = node.getWidth();
4408
+ cfg.height = node.getHeight();
4409
+ cfg.rotation = node.getRotation();
4410
+ if (!cfg.options) {
4411
+ cfg.options = {};
4412
+ }
4413
+ node.getOptionsSFCT(cfg.options);
4414
+ }
4415
+ for (const id in slide.objects) {
4416
+ if (!(id in this._objects)) {
4417
+ delete slide.objects[id];
4418
+ }
4207
4419
  }
4208
- node.getOptionsSFCT(cfg.options);
4209
4420
  } else {
4210
- const options = {};
4211
- node.getOptionsSFCT(options);
4212
- state.objects[id] = {
4213
- type: node.getType(),
4214
- x: node.getX(),
4215
- y: node.getY(),
4216
- width: node.getWidth(),
4217
- height: node.getHeight(),
4218
- rotation: node.getRotation(),
4219
- options
4220
- };
4421
+ state.slides[i] = this._slides[i];
4221
4422
  }
4222
4423
  }
4223
- for (const id in state.objects) {
4224
- if (!(id in this._objects)) {
4225
- delete state.objects[id];
4226
- }
4424
+ }
4425
+ getSelectedSlideId() {
4426
+ return this._selectedSlideId;
4427
+ }
4428
+ setSelectedSlide(slideId) {
4429
+ const idx = this._findSlideIndexById(slideId);
4430
+ if (idx < 0 || slideId === this._selectedSlideId) {
4431
+ return;
4227
4432
  }
4433
+ this._selectedSlideId = slideId;
4434
+ this._pendingState = this._slides[idx]?.objects ?? {};
4435
+ this._pendingStateVersion = this._internalStateVersion;
4436
+ this._invalidate();
4228
4437
  }
4229
4438
  changeCursor(cursor) {
4230
4439
  this._containerNode.style.cursor = cursor;
@@ -4237,6 +4446,9 @@ var RSCanvas = class {
4237
4446
  getScale() {
4238
4447
  return this._scale;
4239
4448
  }
4449
+ toWorldSize(viewSize) {
4450
+ return viewSize / this._scale;
4451
+ }
4240
4452
  toWorldX(viewX) {
4241
4453
  return (viewX - this._translateX) / this._scale;
4242
4454
  }
@@ -4437,6 +4649,11 @@ var RSCanvas = class {
4437
4649
  this._emitStateChanged();
4438
4650
  this._invalidate();
4439
4651
  }
4652
+ forEachObject(fn) {
4653
+ for (const id in this._objects) {
4654
+ fn(this._objects[id]);
4655
+ }
4656
+ }
4440
4657
  async saveToImage(callback) {
4441
4658
  const clone = this._innerContainerNode.cloneNode(true);
4442
4659
  clone.style.transform = "none";